diff --git a/cmd/caddygat/main.go b/cmd/caddygat/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..b4652df67cc53102e76d50d9ba733559bc2260ed
--- /dev/null
+++ b/cmd/caddygat/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+	caddycmd "github.com/caddyserver/caddy/v2/cmd"
+
+	_ "gfx.cafe/gfx/pggat/lib/gat/gatcaddyfile"
+
+	_ "gfx.cafe/gfx/pggat/lib/gat/standard"
+)
+
+func main() {
+	caddycmd.Main()
+}
diff --git a/cmd/caddygat/pgbouncer.go b/cmd/caddygat/pgbouncer.go
new file mode 100644
index 0000000000000000000000000000000000000000..4fe2f957a4c5b74a0040857b730b2c0720245d11
--- /dev/null
+++ b/cmd/caddygat/pgbouncer.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+	"errors"
+	"time"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	caddycmd "github.com/caddyserver/caddy/v2/cmd"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+)
+
+func init() {
+	caddycmd.RegisterCommand(caddycmd.Command{
+		Name:  "pgbouncer",
+		Usage: "<config file>",
+		Short: "Runs in pgbouncer compatibility mode",
+		Func:  runPgbouncer,
+	})
+}
+
+func runPgbouncer(flags caddycmd.Flags) (int, error) {
+	caddy.TrapSignals()
+
+	file := flags.Arg(0)
+	if file == "" {
+		return caddy.ExitCodeFailedStartup, errors.New("usage: pgbouncer <config file>")
+	}
+
+	config, err := pgbouncer.Load(file)
+	if err != nil {
+		return caddy.ExitCodeFailedStartup, err
+	}
+
+	var pggat gat.Config
+	pggat.StatLogPeriod = dur.Duration(time.Second)
+
+	var server gat.ServerConfig
+	server.Listen = config.Listen()
+	server.Routes = append(server.Routes, gat.RouteConfig{
+		Handle: caddyconfig.JSONModuleObject(
+			pgbouncer.Module{
+				ConfigFile: file,
+			},
+			"handler",
+			"pgbouncer",
+			nil,
+		),
+	})
+	pggat.Servers = append(pggat.Servers, server)
+
+	caddyConfig := caddy.Config{
+		AppsRaw: caddy.ModuleMap{
+			"pggat": caddyconfig.JSON(pggat, nil),
+		},
+	}
+
+	if err = caddy.Run(&caddyConfig); err != nil {
+		return caddy.ExitCodeFailedStartup, err
+	}
+
+	select {}
+}
diff --git a/cmd/cgat/main.go b/cmd/cgat/main.go
deleted file mode 100644
index 3963519cda736d7e97a9417e97d71e339dc6b256..0000000000000000000000000000000000000000
--- a/cmd/cgat/main.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package main
-
-import (
-	"errors"
-	"net/http"
-	_ "net/http/pprof"
-	"os"
-	"time"
-
-	"tuxpa.in/a/zlog/log"
-
-	"gfx.cafe/gfx/pggat/lib/gat"
-	"gfx.cafe/gfx/pggat/lib/gat/metrics"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/cloud_sql_discovery"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/digitalocean_discovery"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/pgbouncer"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/ssl_endpoint"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/zalando"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/zalando_operator_discovery"
-)
-
-func loadModule(mode string) (gat.Module, error) {
-	switch mode {
-	case "pggat":
-		conf, err := pgbouncer.Load(os.Args[1])
-		if err != nil {
-			return nil, err
-		}
-		return pgbouncer.NewModule(conf)
-	case "pgbouncer":
-		conf, err := pgbouncer.Load(os.Args[1])
-		if err != nil {
-			return nil, err
-		}
-		return pgbouncer.NewModule(conf)
-	case "pgbouncer_spilo":
-		conf, err := zalando.Load()
-		if err != nil {
-			return nil, err
-		}
-		return zalando.NewModule(conf)
-	case "zalando_kubernetes_operator":
-		conf, err := zalando_operator_discovery.Load()
-		if err != nil {
-			return nil, err
-		}
-		return zalando_operator_discovery.NewModule(conf)
-	case "google_cloud_sql":
-		conf, err := cloud_sql_discovery.Load()
-		if err != nil {
-			return nil, err
-		}
-		return cloud_sql_discovery.NewModule(conf)
-	case "digitalocean_databases":
-		conf, err := digitalocean_discovery.Load()
-		if err != nil {
-			return nil, err
-		}
-		return digitalocean_discovery.NewModule(conf)
-	default:
-		return nil, errors.New("Unknown PGGAT_RUN_MODE: " + mode)
-	}
-}
-
-func main() {
-	go func() {
-		panic(http.ListenAndServe(":8080", nil))
-	}()
-
-	runMode := os.Getenv("PGGAT_RUN_MODE")
-	if runMode == "" {
-		runMode = "pgbouncer"
-	}
-
-	log.Printf("Starting pggat (%s)...", runMode)
-
-	var server gat.Server
-
-	// load and add main module
-	module, err := loadModule(runMode)
-	if err != nil {
-		panic(err)
-	}
-	server.AddModule(module)
-
-	// back up ssl endpoint (for modules that don't have endpoints by default such as discovery)
-	ep, err := ssl_endpoint.NewModule()
-	if err != nil {
-		panic(err)
-	}
-	server.AddModule(ep)
-
-	go func() {
-		var m metrics.Server
-		for {
-			time.Sleep(1 * time.Minute)
-			server.ReadMetrics(&m)
-			log.Printf("%s", m.String())
-			m.Clear()
-		}
-	}()
-
-	err = server.ListenAndServe()
-	if err != nil {
-		panic(err)
-	}
-	return
-}
diff --git a/entrypoint.sh b/entrypoint.sh
index 61f26ffc99437cabcc2421fb715dfa125df0f63e..d0263cf346bc950b7e09a98665ac8e4b1b06717f 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -3,18 +3,4 @@ set -e
 
 EXEC_BIN_PATH=${PGGAT_BIN_PATH:=/usr/bin/pggat}
 
-pggat() {
-    exec $EXEC_BIN_PATH ${@}
-}
-
-
-case "${1}" in
-    "")
-        pggat ${@}
-        ;;
-    *)
-        export PGGAT_RUN_MODE=${1}
-        shift
-        pggat ${@}
-        ;;
-esac
+$EXEC_BIN_PATH run --adapter="caddyfile" --config="/presets/${PGGAT_RUN_MODE:=default}.Caddyfile"
diff --git a/examples/manual_routing.Caddyfile b/examples/manual_routing.Caddyfile
new file mode 100644
index 0000000000000000000000000000000000000000..5ebad0b36394483a3195efe5185bb1c12fd064a6
--- /dev/null
+++ b/examples/manual_routing.Caddyfile
@@ -0,0 +1,42 @@
+{
+	stat_log_period 1s
+}
+
+:6432 {
+	ssl self_signed
+	require_ssl
+
+	@bob {
+		user bob
+	}
+	password @bob password123
+
+	@jeff {
+		user jeff
+	}
+	password @jeff password456
+
+	@other {
+		not or {
+			user jeff
+			user bob
+		}
+	}
+
+	error @other "you are not bob or jeff"
+
+	pool /bobland localhost:5432 bobland postgres password
+	pool /jeffland {
+		pooler session
+
+		address localhost:5432
+		ssl require insecure_skip_verify
+
+		database jeffland
+		username postgres
+		password password
+
+		parameter TimeZone=America/Chicago
+		parameter myParameter=FooBarBaz
+	}
+}
diff --git a/go.mod b/go.mod
index f0ca51a41eda75669c74dcaf7e93c71ada04ce1b..706ee3ef0a02205ea95d65c8de93efd4799b8496 100644
--- a/go.mod
+++ b/go.mod
@@ -4,64 +4,90 @@ go 1.20
 
 require (
 	gfx.cafe/ghalliday1/scram v0.0.2
-	gfx.cafe/util/go/gun v0.0.0-20230721185457-c559e86c829c
+	github.com/caddyserver/caddy/v2 v2.7.4
 	github.com/digitalocean/godo v1.102.1
 	github.com/google/uuid v1.3.0
 	github.com/zalando/postgres-operator v1.8.2
-	google.golang.org/api v0.30.0
+	go.uber.org/zap v1.25.0
+	google.golang.org/api v0.126.0
 	k8s.io/apimachinery v0.27.4
 	k8s.io/client-go v0.27.4
-	tuxpa.in/a/zlog v1.61.0
 )
 
 require (
-	cloud.google.com/go v0.65.0 // indirect
-	github.com/cristalhq/aconfig v0.18.3 // indirect
-	github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 // indirect
+	cloud.google.com/go/compute v1.20.1 // indirect
+	cloud.google.com/go/compute/metadata v0.2.3 // indirect
+	github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/caddyserver/certmagic v0.19.2 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
-	github.com/go-logr/logr v1.2.3 // indirect
+	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-openapi/jsonpointer v0.19.6 // indirect
 	github.com/go-openapi/jsonreference v0.20.1 // indirect
 	github.com/go-openapi/swag v0.22.3 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/mock v1.6.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/gnostic v0.5.7-v3refs // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.1.0 // indirect
-	github.com/googleapis/gax-go/v2 v2.0.5 // indirect
+	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // 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.11.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
-	github.com/imdario/mergo v0.3.6 // indirect
-	github.com/joho/godotenv v1.4.0 // indirect
+	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
 	github.com/kr/text v0.2.0 // indirect
+	github.com/libdns/libdns v0.2.1 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/mattn/go-colorable v0.1.12 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+	github.com/mholt/acmez v1.2.0 // indirect
+	github.com/miekg/dns v1.1.55 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/rs/zerolog v1.28.0 // indirect
-	github.com/sirupsen/logrus v1.9.0 // indirect
+	github.com/onsi/ginkgo/v2 v2.9.5 // indirect
+	github.com/prometheus/client_golang v1.14.0 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.37.0 // indirect
+	github.com/prometheus/procfs v0.8.0 // indirect
+	github.com/quic-go/qpack v0.4.0 // indirect
+	github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
+	github.com/quic-go/quic-go v0.37.5 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/sirupsen/logrus v1.9.3 // indirect
+	github.com/spf13/cobra v1.7.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/xdg-go/stringprep v1.0.4 // indirect
-	go.opencensus.io v0.22.4 // indirect
-	golang.org/x/crypto v0.8.0 // indirect
-	golang.org/x/net v0.9.0 // indirect
-	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
-	golang.org/x/sys v0.7.0 // indirect
-	golang.org/x/term v0.7.0 // indirect
+	github.com/zeebo/blake3 v0.2.3 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/crypto v0.12.0 // indirect
+	golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect
+	golang.org/x/mod v0.11.0 // indirect
+	golang.org/x/net v0.14.0 // indirect
+	golang.org/x/oauth2 v0.8.0 // indirect
+	golang.org/x/sys v0.11.0 // indirect
+	golang.org/x/term v0.11.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
+	golang.org/x/tools v0.10.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
-	google.golang.org/grpc v1.47.0 // indirect
-	google.golang.org/protobuf v1.28.1 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
+	google.golang.org/grpc v1.56.2 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index f571d8eb76a8e8bf72cda03eb9b80b95fb2901d9..91e67319bb9289efbb9802a3f740a5306fbb6e00 100644
--- a/go.sum
+++ b/go.sum
@@ -12,7 +12,6 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP
 cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
 cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
 cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
 cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
@@ -20,6 +19,10 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
+cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -32,22 +35,32 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-gfx.cafe/ghalliday1/scram v0.0.1 h1:CUhXNgquodVk+8hIUYGzqQg585T+yQ4cRs2RU6wG2tA=
-gfx.cafe/ghalliday1/scram v0.0.1/go.mod h1:qt6+WJoFcX2id67G5w+/dktBJ56Se0sZAa7WjqfNNu0=
 gfx.cafe/ghalliday1/scram v0.0.2 h1:uuScaL7DUEP/lKWJnA5kKHq5RJev26q8DMbP3gKviAg=
 gfx.cafe/ghalliday1/scram v0.0.2/go.mod h1:qt6+WJoFcX2id67G5w+/dktBJ56Se0sZAa7WjqfNNu0=
-gfx.cafe/util/go/gun v0.0.0-20230721185457-c559e86c829c h1:4XxKaHfYPam36FibTiy1Te7ycfW4+ys08WYyDih5VmU=
-gfx.cafe/util/go/gun v0.0.0-20230721185457-c559e86c829c/go.mod h1:zxq7FdmfdrI4oGeze0MPJt9WqdkFj3BDDSAWRuB63JQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
-github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
+github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/caddyserver/caddy/v2 v2.7.4 h1:J8nisjdOxnYHXlorUKXY75Gr6iBfudfoGhrJ8t7/flI=
+github.com/caddyserver/caddy/v2 v2.7.4/go.mod h1:/OH2g/56QCSCajEWsFa8kjwacziG/YFxeWgKacnK6KE=
+github.com/caddyserver/certmagic v0.19.2 h1:HZd1AKLx4592MalEGQS39DKs2ZOAJCEM/xYPMQ2/ui0=
+github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
 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/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=
@@ -55,52 +68,51 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+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-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
-github.com/cristalhq/aconfig v0.18.3 h1:Or12LIWIF+2mQpcGWA2PQnNc55+WiHFAqRjYh/pQNtM=
-github.com/cristalhq/aconfig v0.18.3/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
-github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 h1:HG2ql5fGe4FLL2fUv6o+o0YRyF1mWEcYkNfWGWD82k4=
-github.com/cristalhq/aconfig/aconfigdotenv v0.17.1/go.mod h1:gQIKkh+HkVcODvMNz/cLbH65Pk9b0r4tfolCOsI8G9I=
 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=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/digitalocean/godo v1.102.1 h1:BrNePwIXjQWjOJXVTBqkURMjm70BRR0qXbRKfHNBF24=
 github.com/digitalocean/godo v1.102.1/go.mod h1:SaUYccN7r+CO1QtsbXGypAsgobDrmSfVMJESEfXgoEg=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
 github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
-github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
 github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=
 github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
 github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
 github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -116,6 +128,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -136,7 +150,6 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
 github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
 github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -147,8 +160,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@@ -166,18 +180,19 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
+github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
-github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
+github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
@@ -188,20 +203,31 @@ github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
-github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
-github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 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/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
+github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+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=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -210,95 +236,137 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
+github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
+github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
+github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
+github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
+github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d h1:LznySqW8MqVeFh+pW6rOkFdld9QQ7jRydBKKM6jyPVI=
 github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d/go.mod h1:u3hJ0kqCQu/cPpsu3RbCOPZ0d7V3IjPjv1adNRleM9I=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
-github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
-github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+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=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
+github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
+github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
+github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
+github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg=
+github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/quic-go/quic-go v0.37.5 h1:pzkYe8AgaxHi+7KJrYBMF+u2rLO5a9kwyCp2dAsljzk=
+github.com/quic-go/quic-go v0.37.5/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
-github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+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.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zalando/postgres-operator v1.8.2 h1:3FW3j2gXua1MSeE+NiSvB8cxM7k7fyoun46G1v++CCA=
 github.com/zalando/postgres-operator v1.8.2/go.mod h1:f7AXk8LO/tWFdW4myPJZCwMueGg6fI4RqTuOA0BefZE=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
+github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
+github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
+github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
+github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
+github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=
-go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
-go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
-go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
-go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
-go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
-go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=
-go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
+go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
-golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -309,6 +377,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw=
+golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -329,9 +399,13 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/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.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
+golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -339,6 +413,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -357,19 +432,25 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 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-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-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -379,11 +460,17 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 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=
@@ -392,6 +479,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -404,32 +492,35 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20210927094055-39ccf1dd6fa6/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=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
@@ -481,8 +572,10 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
 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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
+golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
+golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
 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=
@@ -502,8 +595,9 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
 google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=
+google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -543,8 +637,10 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I=
-google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8=
+google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -558,10 +654,11 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
-google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
+google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -574,10 +671,9 @@ 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -586,10 +682,13 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 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=
@@ -626,5 +725,3 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kF
 sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
 sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
 sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
-tuxpa.in/a/zlog v1.61.0 h1:7wrS6G4QwpnOmgHRQknrr7IgiMXrfGpekkU0PjM9FhE=
-tuxpa.in/a/zlog v1.61.0/go.mod h1:CNpMe8laDHLSypx/DyxfX1S0oyxUydeo3aGTEbtRBhg=
diff --git a/lib/bouncer/backends/v0/accept.go b/lib/bouncer/backends/v0/accept.go
index 3168ce0a0b6923aa3f7a66bf0871d6fb13efcd39..d094f2f19f5f39bf5b38ca68f71a7a589ba7c5c9 100644
--- a/lib/bouncer/backends/v0/accept.go
+++ b/lib/bouncer/backends/v0/accept.go
@@ -1,16 +1,18 @@
 package backends
 
 import (
+	"crypto/tls"
 	"errors"
 	"io"
 
 	"gfx.cafe/gfx/pggat/lib/auth"
+	"gfx.cafe/gfx/pggat/lib/bouncer"
 	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-func authenticationSASLChallenge(ctx *AcceptContext, encoder auth.SASLEncoder) (done bool, err error) {
+func authenticationSASLChallenge(ctx *acceptContext, encoder auth.SASLEncoder) (done bool, err error) {
 	ctx.Packet, err = ctx.Conn.ReadPacket(true, ctx.Packet)
 	if err != nil {
 		return
@@ -54,7 +56,7 @@ func authenticationSASLChallenge(ctx *AcceptContext, encoder auth.SASLEncoder) (
 	}
 }
 
-func authenticationSASL(ctx *AcceptContext, mechanisms []string, creds auth.SASLClient) error {
+func authenticationSASL(ctx *acceptContext, mechanisms []string, creds auth.SASLClient) error {
 	mechanism, encoder, err := creds.EncodeSASL(mechanisms)
 	if err != nil {
 		return err
@@ -89,7 +91,7 @@ func authenticationSASL(ctx *AcceptContext, mechanisms []string, creds auth.SASL
 	return nil
 }
 
-func authenticationMD5(ctx *AcceptContext, salt [4]byte, creds auth.MD5Client) error {
+func authenticationMD5(ctx *acceptContext, salt [4]byte, creds auth.MD5Client) error {
 	pw := packets.PasswordMessage{
 		Password: creds.EncodeMD5(salt),
 	}
@@ -101,7 +103,7 @@ func authenticationMD5(ctx *AcceptContext, salt [4]byte, creds auth.MD5Client) e
 	return nil
 }
 
-func authenticationCleartext(ctx *AcceptContext, creds auth.CleartextClient) error {
+func authenticationCleartext(ctx *acceptContext, creds auth.CleartextClient) error {
 	pw := packets.PasswordMessage{
 		Password: creds.EncodeCleartext(),
 	}
@@ -113,13 +115,14 @@ func authenticationCleartext(ctx *AcceptContext, creds auth.CleartextClient) err
 	return nil
 }
 
-func authentication(ctx *AcceptContext) (done bool, err error) {
+func authentication(ctx *acceptContext) (done bool, err error) {
 	var method int32
 	ctx.Packet.ReadInt32(&method)
 	// they have more authentication methods than there are pokemon
 	switch method {
 	case 0:
 		// we're good to go, that was easy
+		ctx.Conn.Authenticated = true
 		return true, nil
 	case 2:
 		err = errors.New("kerberos v5 is not supported")
@@ -170,7 +173,7 @@ func authentication(ctx *AcceptContext) (done bool, err error) {
 	}
 }
 
-func startup0(ctx *AcceptContext) (done bool, err error) {
+func startup0(ctx *acceptContext) (done bool, err error) {
 	ctx.Packet, err = ctx.Conn.ReadPacket(true, ctx.Packet)
 	if err != nil {
 		return
@@ -197,7 +200,7 @@ func startup0(ctx *AcceptContext) (done bool, err error) {
 	}
 }
 
-func startup1(ctx *AcceptContext, params *AcceptParams) (done bool, err error) {
+func startup1(ctx *acceptContext) (done bool, err error) {
 	ctx.Packet, err = ctx.Conn.ReadPacket(true, ctx.Packet)
 	if err != nil {
 		return
@@ -205,7 +208,7 @@ func startup1(ctx *AcceptContext, params *AcceptParams) (done bool, err error) {
 
 	switch ctx.Packet.Type() {
 	case packets.TypeBackendKeyData:
-		ctx.Packet.ReadBytes(params.BackendKey[:])
+		ctx.Packet.ReadBytes(ctx.Conn.BackendKey[:])
 		return false, nil
 	case packets.TypeParameterStatus:
 		var ps packets.ParameterStatus
@@ -214,10 +217,10 @@ func startup1(ctx *AcceptContext, params *AcceptParams) (done bool, err error) {
 			return
 		}
 		ikey := strutil.MakeCIString(ps.Key)
-		if params.InitialParameters == nil {
-			params.InitialParameters = make(map[strutil.CIString]string)
+		if ctx.Conn.InitialParameters == nil {
+			ctx.Conn.InitialParameters = make(map[strutil.CIString]string)
 		}
-		params.InitialParameters[ikey] = ps.Value
+		ctx.Conn.InitialParameters[ikey] = ps.Value
 		return false, nil
 	case packets.TypeReadyForQuery:
 		return true, nil
@@ -238,7 +241,7 @@ func startup1(ctx *AcceptContext, params *AcceptParams) (done bool, err error) {
 	}
 }
 
-func enableSSL(ctx *AcceptContext) (bool, error) {
+func enableSSL(ctx *acceptContext) (bool, error) {
 	ctx.Packet = ctx.Packet.Reset(0, 4)
 	ctx.Packet = ctx.Packet.AppendUint16(1234)
 	ctx.Packet = ctx.Packet.AppendUint16(5679)
@@ -246,7 +249,7 @@ func enableSSL(ctx *AcceptContext) (bool, error) {
 		return false, err
 	}
 
-	byteReader, ok := ctx.Conn.(io.ByteReader)
+	byteReader, ok := ctx.Conn.ReadWriteCloser.(io.ByteReader)
 	if !ok {
 		return false, errors.New("server must be io.ByteReader to enable ssl")
 	}
@@ -262,7 +265,7 @@ func enableSSL(ctx *AcceptContext) (bool, error) {
 		return false, nil
 	}
 
-	sslClient, ok := ctx.Conn.(fed.SSLClient)
+	sslClient, ok := ctx.Conn.ReadWriteCloser.(fed.SSLClient)
 	if !ok {
 		return false, errors.New("server must be fed.SSLClient to enable ssl")
 	}
@@ -274,23 +277,20 @@ func enableSSL(ctx *AcceptContext) (bool, error) {
 	return true, nil
 }
 
-func Accept(ctx *AcceptContext) (AcceptParams, error) {
+func accept(ctx *acceptContext) error {
 	username := ctx.Options.Username
 
 	if ctx.Options.Database == "" {
 		ctx.Options.Database = username
 	}
 
-	var params AcceptParams
-
 	if ctx.Options.SSLMode.ShouldAttempt() {
-		var err error
-		params.SSLEnabled, err = enableSSL(ctx)
+		sslEnabled, err := enableSSL(ctx)
 		if err != nil {
-			return AcceptParams{}, err
+			return err
 		}
-		if !params.SSLEnabled && ctx.Options.SSLMode.IsRequired() {
-			return AcceptParams{}, errors.New("server rejected SSL encryption")
+		if !sslEnabled && ctx.Options.SSLMode.IsRequired() {
+			return errors.New("server rejected SSL encryption")
 		}
 	}
 
@@ -315,14 +315,14 @@ func Accept(ctx *AcceptContext) (AcceptParams, error) {
 
 	err := ctx.Conn.WritePacket(ctx.Packet)
 	if err != nil {
-		return AcceptParams{}, err
+		return err
 	}
 
 	for {
 		var done bool
 		done, err = startup0(ctx)
 		if err != nil {
-			return AcceptParams{}, err
+			return err
 		}
 		if done {
 			break
@@ -331,9 +331,9 @@ func Accept(ctx *AcceptContext) (AcceptParams, error) {
 
 	for {
 		var done bool
-		done, err = startup1(ctx, &params)
+		done, err = startup1(ctx)
 		if err != nil {
-			return AcceptParams{}, err
+			return err
 		}
 		if done {
 			break
@@ -341,5 +341,28 @@ func Accept(ctx *AcceptContext) (AcceptParams, error) {
 	}
 
 	// startup complete, connection is ready for queries
-	return params, nil
+	return nil
+}
+
+func Accept(
+	conn *fed.Conn,
+	sslMode bouncer.SSLMode,
+	sslConfig *tls.Config,
+	username string,
+	credentials auth.Credentials,
+	database string,
+	startupParameters map[strutil.CIString]string,
+) error {
+	ctx := acceptContext{
+		Conn: conn,
+		Options: acceptOptions{
+			SSLMode:           sslMode,
+			SSLConfig:         sslConfig,
+			Username:          username,
+			Credentials:       credentials,
+			Database:          database,
+			StartupParameters: startupParameters,
+		},
+	}
+	return accept(&ctx)
 }
diff --git a/lib/bouncer/backends/v0/cancel.go b/lib/bouncer/backends/v0/cancel.go
index 1ff2e957ab15fc15b14429e55f7656a40e1cf4cf..23769b8dbed161c791b0e64ee450820a24d8bb9d 100644
--- a/lib/bouncer/backends/v0/cancel.go
+++ b/lib/bouncer/backends/v0/cancel.go
@@ -2,7 +2,7 @@ package backends
 
 import "gfx.cafe/gfx/pggat/lib/fed"
 
-func Cancel(server fed.ReadWriter, key [8]byte) error {
+func Cancel(server *fed.Conn, key [8]byte) error {
 	packet := fed.NewPacket(0, 12)
 	packet = packet.AppendUint16(1234)
 	packet = packet.AppendUint16(5678)
diff --git a/lib/bouncer/backends/v0/context.go b/lib/bouncer/backends/v0/context.go
index 5d4f08c717962c2d1eec88edd092de5feab717be..01945b65e47630a4bdfa299abf446d1bbdc7f05c 100644
--- a/lib/bouncer/backends/v0/context.go
+++ b/lib/bouncer/backends/v0/context.go
@@ -4,38 +4,38 @@ import (
 	"gfx.cafe/gfx/pggat/lib/fed"
 )
 
-type AcceptContext struct {
+type acceptContext struct {
 	Packet  fed.Packet
-	Conn    fed.Conn
-	Options AcceptOptions
+	Conn    *fed.Conn
+	Options acceptOptions
 }
 
-type Context struct {
-	Server    fed.ReadWriter
+type context struct {
+	Server    *fed.Conn
 	Packet    fed.Packet
-	Peer      fed.ReadWriter
+	Peer      *fed.Conn
 	PeerError error
 	TxState   byte
 }
 
-func (T *Context) ServerRead() error {
+func (T *context) ServerRead() error {
 	var err error
 	T.Packet, err = T.Server.ReadPacket(true, T.Packet)
 	return err
 }
 
-func (T *Context) ServerWrite() error {
+func (T *context) ServerWrite() error {
 	return T.Server.WritePacket(T.Packet)
 }
 
-func (T *Context) PeerOK() bool {
+func (T *context) PeerOK() bool {
 	if T == nil {
 		return false
 	}
 	return T.Peer != nil && T.PeerError == nil
 }
 
-func (T *Context) PeerFail(err error) {
+func (T *context) PeerFail(err error) {
 	if T == nil {
 		return
 	}
@@ -43,7 +43,7 @@ func (T *Context) PeerFail(err error) {
 	T.PeerError = err
 }
 
-func (T *Context) PeerRead() bool {
+func (T *context) PeerRead() bool {
 	if T == nil {
 		return false
 	}
@@ -59,7 +59,7 @@ func (T *Context) PeerRead() bool {
 	return true
 }
 
-func (T *Context) PeerWrite() {
+func (T *context) PeerWrite() {
 	if T == nil {
 		return
 	}
diff --git a/lib/bouncer/backends/v0/options.go b/lib/bouncer/backends/v0/options.go
index 5da0b1e9ff38afe1c0707d50dea63915d4b8a055..9d98d466be8d1eb0219b78a869f09d646b37f274 100644
--- a/lib/bouncer/backends/v0/options.go
+++ b/lib/bouncer/backends/v0/options.go
@@ -8,7 +8,7 @@ import (
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-type AcceptOptions struct {
+type acceptOptions struct {
 	SSLMode           bouncer.SSLMode
 	SSLConfig         *tls.Config
 	Username          string
diff --git a/lib/bouncer/backends/v0/params.go b/lib/bouncer/backends/v0/params.go
deleted file mode 100644
index c3f6cb0ece1003bdc382db66106cd4783a80bf4d..0000000000000000000000000000000000000000
--- a/lib/bouncer/backends/v0/params.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package backends
-
-import "gfx.cafe/gfx/pggat/lib/util/strutil"
-
-type AcceptParams struct {
-	SSLEnabled        bool
-	InitialParameters map[strutil.CIString]string
-	BackendKey        [8]byte
-}
diff --git a/lib/bouncer/backends/v0/query.go b/lib/bouncer/backends/v0/query.go
index c968947408f213a445482de8bbe4028cbbe4f385..89a8bd3f82d758a7f924f14d7944e0285e4376ef 100644
--- a/lib/bouncer/backends/v0/query.go
+++ b/lib/bouncer/backends/v0/query.go
@@ -3,11 +3,12 @@ package backends
 import (
 	"fmt"
 
+	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-func CopyIn(ctx *Context) error {
+func copyIn(ctx *context) error {
 	ctx.PeerWrite()
 
 	for {
@@ -32,7 +33,7 @@ func CopyIn(ctx *Context) error {
 	}
 }
 
-func CopyOut(ctx *Context) error {
+func copyOut(ctx *context) error {
 	ctx.PeerWrite()
 
 	for {
@@ -56,7 +57,7 @@ func CopyOut(ctx *Context) error {
 	}
 }
 
-func Query(ctx *Context) error {
+func query(ctx *context) error {
 	if err := ctx.ServerWrite(); err != nil {
 		return err
 	}
@@ -78,11 +79,11 @@ func Query(ctx *Context) error {
 			packets.TypeNotificationResponse:
 			ctx.PeerWrite()
 		case packets.TypeCopyInResponse:
-			if err = CopyIn(ctx); err != nil {
+			if err = copyIn(ctx); err != nil {
 				return err
 			}
 		case packets.TypeCopyOutResponse:
-			if err = CopyOut(ctx); err != nil {
+			if err = copyOut(ctx); err != nil {
 				return err
 			}
 		case packets.TypeReadyForQuery:
@@ -99,20 +100,34 @@ func Query(ctx *Context) error {
 	}
 }
 
-func QueryString(ctx *Context, query string) error {
-	q := packets.Query(query)
-	ctx.Packet = q.IntoPacket(ctx.Packet)
-	return Query(ctx)
+func queryString(ctx *context, q string) error {
+	qq := packets.Query(q)
+	ctx.Packet = qq.IntoPacket(ctx.Packet)
+	return query(ctx)
 }
 
-func SetParameter(ctx *Context, name strutil.CIString, value string) error {
+func QueryString(server, peer *fed.Conn, buffer fed.Packet, query string) (err, peerError error, packet fed.Packet) {
+	ctx := context{
+		Server: server,
+		Peer:   peer,
+		Packet: buffer,
+	}
+	err = queryString(&ctx, query)
+	peerError = ctx.PeerError
+	packet = ctx.Packet
+	return
+}
+
+func SetParameter(server, peer *fed.Conn, buffer fed.Packet, name strutil.CIString, value string) (err, peerError error, packet fed.Packet) {
 	return QueryString(
-		ctx,
+		server,
+		peer,
+		buffer,
 		fmt.Sprintf(`SET "%s" = '%s'`, strutil.Escape(name.String(), '"'), strutil.Escape(value, '\'')),
 	)
 }
 
-func FunctionCall(ctx *Context) error {
+func functionCall(ctx *context) error {
 	if err := ctx.ServerWrite(); err != nil {
 		return err
 	}
@@ -144,7 +159,7 @@ func FunctionCall(ctx *Context) error {
 	}
 }
 
-func Sync(ctx *Context) (bool, error) {
+func sync(ctx *context) (bool, error) {
 	ctx.Packet = ctx.Packet.Reset(packets.TypeSync)
 	if err := ctx.ServerWrite(); err != nil {
 		return false, err
@@ -175,13 +190,13 @@ func Sync(ctx *Context) (bool, error) {
 			packets.TypeNotificationResponse:
 			ctx.PeerWrite()
 		case packets.TypeCopyInResponse:
-			if err = CopyIn(ctx); err != nil {
+			if err = copyIn(ctx); err != nil {
 				return false, err
 			}
 			// why
 			return false, nil
 		case packets.TypeCopyOutResponse:
-			if err = CopyOut(ctx); err != nil {
+			if err = copyOut(ctx); err != nil {
 				return false, err
 			}
 		case packets.TypeReadyForQuery:
@@ -198,7 +213,19 @@ func Sync(ctx *Context) (bool, error) {
 	}
 }
 
-func EQP(ctx *Context) error {
+func Sync(server, peer *fed.Conn, buffer fed.Packet) (err, peerErr error, packet fed.Packet) {
+	ctx := context{
+		Server: server,
+		Peer:   peer,
+		Packet: buffer,
+	}
+	_, err = sync(&ctx)
+	peerErr = ctx.PeerError
+	packet = ctx.Packet
+	return
+}
+
+func eqp(ctx *context) error {
 	if err := ctx.ServerWrite(); err != nil {
 		return err
 	}
@@ -206,7 +233,7 @@ func EQP(ctx *Context) error {
 	for {
 		if !ctx.PeerRead() {
 			for {
-				ok, err := Sync(ctx)
+				ok, err := sync(ctx)
 				if err != nil {
 					return err
 				}
@@ -218,7 +245,7 @@ func EQP(ctx *Context) error {
 
 		switch ctx.Packet.Type() {
 		case packets.TypeSync:
-			ok, err := Sync(ctx)
+			ok, err := sync(ctx)
 			if err != nil {
 				return err
 			}
@@ -235,18 +262,15 @@ func EQP(ctx *Context) error {
 	}
 }
 
-func Transaction(ctx *Context) error {
-	if ctx.TxState == '\x00' {
-		ctx.TxState = 'I'
-	}
+func transaction(ctx *context) error {
 	for {
 		switch ctx.Packet.Type() {
 		case packets.TypeQuery:
-			if err := Query(ctx); err != nil {
+			if err := query(ctx); err != nil {
 				return err
 			}
 		case packets.TypeFunctionCall:
-			if err := FunctionCall(ctx); err != nil {
+			if err := functionCall(ctx); err != nil {
 				return err
 			}
 		case packets.TypeSync:
@@ -255,7 +279,7 @@ func Transaction(ctx *Context) error {
 			ctx.Packet = rfq.IntoPacket(ctx.Packet)
 			ctx.PeerWrite()
 		case packets.TypeParse, packets.TypeBind, packets.TypeClose, packets.TypeDescribe, packets.TypeExecute, packets.TypeFlush:
-			if err := EQP(ctx); err != nil {
+			if err := eqp(ctx); err != nil {
 				return err
 			}
 		default:
@@ -268,7 +292,7 @@ func Transaction(ctx *Context) error {
 
 		if !ctx.PeerRead() {
 			// abort tx
-			err := QueryString(ctx, "ABORT;")
+			err := queryString(ctx, "ABORT;")
 			if err != nil {
 				return err
 			}
@@ -280,3 +304,15 @@ func Transaction(ctx *Context) error {
 		}
 	}
 }
+
+func Transaction(server, peer *fed.Conn, initialPacket fed.Packet) (err, peerError error, packet fed.Packet) {
+	ctx := context{
+		Server: server,
+		Peer:   peer,
+		Packet: initialPacket,
+	}
+	err = transaction(&ctx)
+	peerError = ctx.PeerError
+	packet = ctx.Packet
+	return
+}
diff --git a/lib/bouncer/bouncers/v2/bouncer.go b/lib/bouncer/bouncers/v2/bouncer.go
index 797ba8095b18d139e7b43515afe02c7805866ef2..573b8cc28a3ac92de6dcf1c5a03cfa5ace827877 100644
--- a/lib/bouncer/bouncers/v2/bouncer.go
+++ b/lib/bouncer/bouncers/v2/bouncer.go
@@ -7,31 +7,24 @@ import (
 	"gfx.cafe/gfx/pggat/lib/perror"
 )
 
-func clientFail(ctx *backends.Context, client fed.ReadWriter, err perror.Error) {
+func clientFail(packet fed.Packet, client *fed.Conn, err perror.Error) fed.Packet {
 	// send fatal error to client
 	resp := packets.ErrorResponse{
 		Error: err,
 	}
-	ctx.Packet = resp.IntoPacket(ctx.Packet)
-	_ = client.WritePacket(ctx.Packet)
+	packet = resp.IntoPacket(packet)
+	_ = client.WritePacket(packet)
+	return packet
 }
 
-func Bounce(client, server fed.ReadWriter, initialPacket fed.Packet) (packet fed.Packet, clientError error, serverError error) {
-	ctx := backends.Context{
-		Server: server,
-		Packet: initialPacket,
-		Peer:   client,
-	}
-	serverError = backends.Transaction(&ctx)
-	clientError = ctx.PeerError
+func Bounce(client, server *fed.Conn, initialPacket fed.Packet) (packet fed.Packet, clientError error, serverError error) {
+	serverError, clientError, packet = backends.Transaction(server, client, initialPacket)
 
 	if clientError != nil {
-		clientFail(&ctx, client, perror.Wrap(clientError))
+		packet = clientFail(packet, client, perror.Wrap(clientError))
 	} else if serverError != nil {
-		clientFail(&ctx, client, perror.Wrap(serverError))
+		packet = clientFail(packet, client, perror.Wrap(serverError))
 	}
 
-	packet = ctx.Packet
-
 	return
 }
diff --git a/lib/bouncer/frontends/v0/accept.go b/lib/bouncer/frontends/v0/accept.go
index aff54a78a61da396e892728ff8d7e05cfc509113..5fadb9e3e51ba004a9e06fa86f103944bb7b09fd 100644
--- a/lib/bouncer/frontends/v0/accept.go
+++ b/lib/bouncer/frontends/v0/accept.go
@@ -1,21 +1,20 @@
 package frontends
 
 import (
-	"fmt"
+	"crypto/tls"
 	"io"
 	"strings"
 
 	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/perror"
-	"gfx.cafe/gfx/pggat/lib/util/slices"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
 func startup0(
-	ctx *AcceptContext,
-	params *AcceptParams,
-) (done bool, err perror.Error) {
+	ctx *acceptContext,
+	params *acceptParams,
+) (cancelling bool, done bool, err perror.Error) {
 	var err2 error
 	ctx.Packet, err2 = ctx.Conn.ReadPacket(false, ctx.Packet)
 	if err2 != nil {
@@ -34,22 +33,11 @@ func startup0(
 		case 5678:
 			// Cancel
 			p.ReadBytes(params.CancelKey[:])
-
-			if params.CancelKey == [8]byte{} {
-				// very rare that this would ever happen
-				// and it's ok if we don't honor cancel requests
-				err = perror.New(
-					perror.FATAL,
-					perror.ProtocolViolation,
-					"cancel key cannot be null",
-				)
-				return
-			}
-
+			cancelling = true
 			done = true
 			return
 		case 5679:
-			byteWriter, ok := ctx.Conn.(io.ByteWriter)
+			byteWriter, ok := ctx.Conn.ReadWriteCloser.(io.ByteWriter)
 			if !ok {
 				err = perror.New(
 					perror.FATAL,
@@ -65,13 +53,9 @@ func startup0(
 				return
 			}
 
-			sslServer, ok := ctx.Conn.(fed.SSLServer)
+			sslServer, ok := ctx.Conn.ReadWriteCloser.(fed.SSLServer)
 			if !ok {
-				err = perror.New(
-					perror.FATAL,
-					perror.FeatureNotSupported,
-					"SSL is not supported",
-				)
+				err = perror.Wrap(byteWriter.WriteByte('N'))
 				return
 			}
 
@@ -82,10 +66,9 @@ func startup0(
 			if err = perror.Wrap(sslServer.EnableSSLServer(ctx.Options.SSLConfig)); err != nil {
 				return
 			}
-			params.SSLEnabled = true
 			return
 		case 5680:
-			byteWriter, ok := ctx.Conn.(io.ByteWriter)
+			byteWriter, ok := ctx.Conn.ReadWriteCloser.(io.ByteWriter)
 			if !ok {
 				err = perror.New(
 					perror.FATAL,
@@ -131,9 +114,9 @@ func startup0(
 
 		switch key {
 		case "user":
-			params.User = value
+			ctx.Conn.User = value
 		case "database":
-			params.Database = value
+			ctx.Conn.Database = value
 		case "options":
 			fields := strings.Fields(value)
 			for i := 0; i < len(fields); i++ {
@@ -154,19 +137,10 @@ func startup0(
 
 					ikey := strutil.MakeCIString(key)
 
-					if !slices.Contains(ctx.Options.AllowedStartupOptions, ikey) {
-						err = perror.New(
-							perror.FATAL,
-							perror.FeatureNotSupported,
-							fmt.Sprintf(`Startup parameter "%s" is not allowed`, key),
-						)
-						return
-					}
-
-					if params.InitialParameters == nil {
-						params.InitialParameters = make(map[strutil.CIString]string)
+					if ctx.Conn.InitialParameters == nil {
+						ctx.Conn.InitialParameters = make(map[strutil.CIString]string)
 					}
-					params.InitialParameters[ikey] = value
+					ctx.Conn.InitialParameters[ikey] = value
 				default:
 					err = perror.New(
 						perror.FATAL,
@@ -190,19 +164,10 @@ func startup0(
 			} else {
 				ikey := strutil.MakeCIString(key)
 
-				if !slices.Contains(ctx.Options.AllowedStartupOptions, ikey) {
-					err = perror.New(
-						perror.FATAL,
-						perror.FeatureNotSupported,
-						fmt.Sprintf(`Startup parameter "%s" is not allowed`, key),
-					)
-					return
-				}
-
-				if params.InitialParameters == nil {
-					params.InitialParameters = make(map[strutil.CIString]string)
+				if ctx.Conn.InitialParameters == nil {
+					ctx.Conn.InitialParameters = make(map[strutil.CIString]string)
 				}
-				params.InitialParameters[ikey] = value
+				ctx.Conn.InitialParameters[ikey] = value
 			}
 		}
 	}
@@ -220,7 +185,7 @@ func startup0(
 		}
 	}
 
-	if params.User == "" {
+	if ctx.Conn.User == "" {
 		err = perror.New(
 			perror.FATAL,
 			perror.InvalidAuthorizationSpecification,
@@ -228,20 +193,20 @@ func startup0(
 		)
 		return
 	}
-	if params.Database == "" {
-		params.Database = params.User
+	if ctx.Conn.Database == "" {
+		ctx.Conn.Database = ctx.Conn.User
 	}
 
 	done = true
 	return
 }
 
-func accept(
-	ctx *AcceptContext,
-) (params AcceptParams, err perror.Error) {
+func accept0(
+	ctx *acceptContext,
+) (params acceptParams, err perror.Error) {
 	for {
 		var done bool
-		done, err = startup0(ctx, &params)
+		params.IsCanceling, done, err = startup0(ctx, &params)
 		if err != nil {
 			return
 		}
@@ -250,23 +215,10 @@ func accept(
 		}
 	}
 
-	if params.CancelKey != [8]byte{} {
-		return
-	}
-
-	if ctx.Options.SSLRequired && !params.SSLEnabled {
-		err = perror.New(
-			perror.FATAL,
-			perror.InvalidPassword,
-			"SSL is required",
-		)
-		return
-	}
-
 	return
 }
 
-func fail(packet fed.Packet, client fed.Conn, err perror.Error) {
+func fail(packet fed.Packet, client fed.ReadWriter, err perror.Error) {
 	resp := packets.ErrorResponse{
 		Error: err,
 	}
@@ -274,11 +226,29 @@ func fail(packet fed.Packet, client fed.Conn, err perror.Error) {
 	_ = client.WritePacket(packet)
 }
 
-func Accept(ctx *AcceptContext) (AcceptParams, perror.Error) {
-	params, err := accept(ctx)
+func accept(ctx *acceptContext) (acceptParams, perror.Error) {
+	params, err := accept0(ctx)
 	if err != nil {
 		fail(ctx.Packet, ctx.Conn, err)
-		return AcceptParams{}, err
+		return acceptParams{}, err
 	}
 	return params, nil
 }
+
+func Accept(conn *fed.Conn, tlsConfig *tls.Config) (
+	cancelKey [8]byte,
+	isCanceling bool,
+	err perror.Error,
+) {
+	ctx := acceptContext{
+		Conn: conn,
+		Options: acceptOptions{
+			SSLConfig: tlsConfig,
+		},
+	}
+	var params acceptParams
+	params, err = accept(&ctx)
+	cancelKey = params.CancelKey
+	isCanceling = params.IsCanceling
+	return
+}
diff --git a/lib/bouncer/frontends/v0/authenticate.go b/lib/bouncer/frontends/v0/authenticate.go
index b81603e3f1d59fccb2779a1b9125732c3986ec1c..c22f4e3920af9877f2bceb6ca3d56dc49e4532ac 100644
--- a/lib/bouncer/frontends/v0/authenticate.go
+++ b/lib/bouncer/frontends/v0/authenticate.go
@@ -6,11 +6,12 @@ import (
 	"io"
 
 	"gfx.cafe/gfx/pggat/lib/auth"
+	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/perror"
 )
 
-func authenticationSASLInitial(ctx *AuthenticateContext, creds auth.SASLServer) (tool auth.SASLVerifier, resp []byte, done bool, err perror.Error) {
+func authenticationSASLInitial(ctx *authenticateContext, creds auth.SASLServer) (tool auth.SASLVerifier, resp []byte, done bool, err perror.Error) {
 	// check which authentication method the client wants
 	var err2 error
 	ctx.Packet, err2 = ctx.Conn.ReadPacket(true, ctx.Packet)
@@ -42,7 +43,7 @@ func authenticationSASLInitial(ctx *AuthenticateContext, creds auth.SASLServer)
 	return
 }
 
-func authenticationSASLContinue(ctx *AuthenticateContext, tool auth.SASLVerifier) (resp []byte, done bool, err perror.Error) {
+func authenticationSASLContinue(ctx *authenticateContext, tool auth.SASLVerifier) (resp []byte, done bool, err perror.Error) {
 	var err2 error
 	ctx.Packet, err2 = ctx.Conn.ReadPacket(true, ctx.Packet)
 	if err2 != nil {
@@ -67,7 +68,7 @@ func authenticationSASLContinue(ctx *AuthenticateContext, tool auth.SASLVerifier
 	return
 }
 
-func authenticationSASL(ctx *AuthenticateContext, creds auth.SASLServer) perror.Error {
+func authenticationSASL(ctx *authenticateContext, creds auth.SASLServer) perror.Error {
 	saslInitial := packets.AuthenticationSASL{
 		Mechanisms: creds.SupportedSASLMechanisms(),
 	}
@@ -109,7 +110,7 @@ func authenticationSASL(ctx *AuthenticateContext, creds auth.SASLServer) perror.
 	return nil
 }
 
-func authenticationMD5(ctx *AuthenticateContext, creds auth.MD5Server) perror.Error {
+func authenticationMD5(ctx *authenticateContext, creds auth.MD5Server) perror.Error {
 	var salt [4]byte
 	_, err := rand.Read(salt[:])
 	if err != nil {
@@ -141,7 +142,7 @@ func authenticationMD5(ctx *AuthenticateContext, creds auth.MD5Server) perror.Er
 	return nil
 }
 
-func authenticate(ctx *AuthenticateContext) (params AuthenticateParams, err perror.Error) {
+func authenticate(ctx *authenticateContext) (err perror.Error) {
 	if ctx.Options.Credentials != nil {
 		if credsSASL, ok := ctx.Options.Credentials.(auth.SASLServer); ok {
 			err = authenticationSASL(ctx, credsSASL)
@@ -165,16 +166,17 @@ func authenticate(ctx *AuthenticateContext) (params AuthenticateParams, err perr
 	if err = perror.Wrap(ctx.Conn.WritePacket(ctx.Packet)); err != nil {
 		return
 	}
+	ctx.Conn.Authenticated = true
 
 	// send backend key data
-	_, err2 := rand.Read(params.BackendKey[:])
+	_, err2 := rand.Read(ctx.Conn.BackendKey[:])
 	if err2 != nil {
 		err = perror.Wrap(err2)
 		return
 	}
 
 	keyData := packets.BackendKeyData{
-		CancellationKey: params.BackendKey,
+		CancellationKey: ctx.Conn.BackendKey,
 	}
 	ctx.Packet = keyData.IntoPacket(ctx.Packet)
 	if err = perror.Wrap(ctx.Conn.WritePacket(ctx.Packet)); err != nil {
@@ -184,11 +186,18 @@ func authenticate(ctx *AuthenticateContext) (params AuthenticateParams, err perr
 	return
 }
 
-func Authenticate(ctx *AuthenticateContext) (AuthenticateParams, perror.Error) {
-	params, err := authenticate(ctx)
-	if err != nil {
-		fail(ctx.Packet, ctx.Conn, err)
-		return AuthenticateParams{}, err
+func Authenticate(conn *fed.Conn, creds auth.Credentials) (err perror.Error) {
+	if conn.Authenticated {
+		// already authenticated
+		return
 	}
-	return params, nil
+
+	ctx := authenticateContext{
+		Conn: conn,
+		Options: authenticateOptions{
+			Credentials: creds,
+		},
+	}
+	err = authenticate(&ctx)
+	return
 }
diff --git a/lib/bouncer/frontends/v0/context.go b/lib/bouncer/frontends/v0/context.go
index 859532528081af9edecf9d1ca3bb9bfbb2adb3da..e0668cef9a24a5c7287da9562012e47f27cda543 100644
--- a/lib/bouncer/frontends/v0/context.go
+++ b/lib/bouncer/frontends/v0/context.go
@@ -2,14 +2,14 @@ package frontends
 
 import "gfx.cafe/gfx/pggat/lib/fed"
 
-type AcceptContext struct {
+type acceptContext struct {
 	Packet  fed.Packet
-	Conn    fed.Conn
-	Options AcceptOptions
+	Conn    *fed.Conn
+	Options acceptOptions
 }
 
-type AuthenticateContext struct {
+type authenticateContext struct {
 	Packet  fed.Packet
-	Conn    fed.Conn
-	Options AuthenticateOptions
+	Conn    *fed.Conn
+	Options authenticateOptions
 }
diff --git a/lib/bouncer/frontends/v0/options.go b/lib/bouncer/frontends/v0/options.go
index b06d9889b7cb9f7d13ebbfb5413e080fcb9d6eff..304f7b82256efcd4edf0dc1d7125c25491f93320 100644
--- a/lib/bouncer/frontends/v0/options.go
+++ b/lib/bouncer/frontends/v0/options.go
@@ -4,15 +4,12 @@ import (
 	"crypto/tls"
 
 	"gfx.cafe/gfx/pggat/lib/auth"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-type AcceptOptions struct {
-	SSLRequired           bool
-	SSLConfig             *tls.Config
-	AllowedStartupOptions []strutil.CIString
+type acceptOptions struct {
+	SSLConfig *tls.Config
 }
 
-type AuthenticateOptions struct {
+type authenticateOptions struct {
 	Credentials auth.Credentials
 }
diff --git a/lib/bouncer/frontends/v0/params.go b/lib/bouncer/frontends/v0/params.go
index a182469535682589611815d51acd31be83e4acd0..0d960dbf3edbad11e492c22e899dd71e08824172 100644
--- a/lib/bouncer/frontends/v0/params.go
+++ b/lib/bouncer/frontends/v0/params.go
@@ -1,18 +1,6 @@
 package frontends
 
-import "gfx.cafe/gfx/pggat/lib/util/strutil"
-
-type AcceptParams struct {
-	CancelKey [8]byte
-
-	// or
-
-	SSLEnabled        bool
-	User              string
-	Database          string
-	InitialParameters map[strutil.CIString]string
-}
-
-type AuthenticateParams struct {
-	BackendKey [8]byte
+type acceptParams struct {
+	CancelKey   [8]byte
+	IsCanceling bool
 }
diff --git a/lib/fed/conn.go b/lib/fed/conn.go
index 394e451bdfa0b6e8c393900d4e3ee1f507aae460..c0b8e990d29292906f42356c127a0b1e7cce87db 100644
--- a/lib/fed/conn.go
+++ b/lib/fed/conn.go
@@ -1,123 +1,61 @@
 package fed
 
 import (
-	"bufio"
-	"crypto/tls"
-	"encoding/binary"
-	"errors"
-	"io"
-	"net"
-
-	"gfx.cafe/gfx/pggat/lib/util/slices"
+	"gfx.cafe/gfx/pggat/lib/util/decorator"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-type Conn interface {
-	ReadWriter
-
-	Close() error
-}
+type Conn struct {
+	noCopy decorator.NoCopy
 
-type netConn struct {
-	conn   net.Conn
-	writer bufio.Writer
-	reader bufio.Reader
+	ReadWriteCloser
 
-	headerBuf [5]byte
-}
+	Middleware []Middleware
 
-func WrapNetConn(conn net.Conn) Conn {
-	c := &netConn{
-		conn: conn,
-	}
-	c.writer.Reset(conn)
-	c.reader.Reset(conn)
-	return c
+	User              string
+	Database          string
+	InitialParameters map[strutil.CIString]string
+	Authenticated     bool
+	BackendKey        [8]byte
 }
 
-func (T *netConn) EnableSSLClient(config *tls.Config) error {
-	if err := T.writer.Flush(); err != nil {
-		return err
-	}
-	if T.reader.Buffered() > 0 {
-		return errors.New("expected empty read buffer")
+func NewConn(rw ReadWriteCloser) *Conn {
+	return &Conn{
+		ReadWriteCloser: rw,
 	}
-	sslConn := tls.Client(T.conn, config)
-	T.writer.Reset(sslConn)
-	T.reader.Reset(sslConn)
-	T.conn = sslConn
-	return sslConn.Handshake()
 }
 
-func (T *netConn) EnableSSLServer(config *tls.Config) error {
-	if err := T.writer.Flush(); err != nil {
-		return err
-	}
-	if T.reader.Buffered() > 0 {
-		return errors.New("expected empty read buffer")
-	}
-	sslConn := tls.Server(T.conn, config)
-	T.writer.Reset(sslConn)
-	T.reader.Reset(sslConn)
-	T.conn = sslConn
-	return sslConn.Handshake()
-}
-
-func (T *netConn) ReadByte() (byte, error) {
-	if err := T.writer.Flush(); err != nil {
-		return 0, err
-	}
-	return T.reader.ReadByte()
-}
-
-func (T *netConn) ReadPacket(typed bool, buffer Packet) (packet Packet, err error) {
+func (T *Conn) ReadPacket(typed bool, buffer Packet) (packet Packet, err error) {
 	packet = buffer
-
-	if err = T.writer.Flush(); err != nil {
-		return
-	}
-
-	if typed {
-		_, err = io.ReadFull(&T.reader, T.headerBuf[:])
+	for {
+		packet, err = T.ReadWriteCloser.ReadPacket(typed, buffer)
 		if err != nil {
 			return
 		}
-	} else {
-		_, err = io.ReadFull(&T.reader, T.headerBuf[1:])
-		if err != nil {
+		for _, middleware := range T.Middleware {
+			packet, err = middleware.ReadPacket(packet)
+			if err != nil {
+				return
+			}
+			if len(packet) == 0 {
+				break
+			}
+		}
+		if len(packet) != 0 {
 			return
 		}
 	}
-
-	length := binary.BigEndian.Uint32(T.headerBuf[1:])
-
-	packet = slices.Resize(buffer, int(length)+1)
-	copy(packet, T.headerBuf[:])
-
-	_, err = io.ReadFull(&T.reader, packet.Payload())
-	if err != nil {
-		return
-	}
-	return
 }
 
-func (T *netConn) WriteByte(b byte) error {
-	return T.writer.WriteByte(b)
-}
-
-func (T *netConn) WritePacket(packet Packet) error {
-	_, err := T.writer.Write(packet.Bytes())
-	return err
-}
-
-func (T *netConn) Close() error {
-	if err := T.writer.Flush(); err != nil {
-		return err
+func (T *Conn) WritePacket(packet Packet) (err error) {
+	for _, middleware := range T.Middleware {
+		packet, err = middleware.WritePacket(packet)
+		if err != nil || len(packet) == 0 {
+			return
+		}
 	}
-	return T.conn.Close()
+	err = T.ReadWriteCloser.WritePacket(packet)
+	return
 }
 
-var _ Conn = (*netConn)(nil)
-var _ SSLServer = (*netConn)(nil)
-var _ SSLClient = (*netConn)(nil)
-var _ io.ByteReader = (*netConn)(nil)
-var _ io.ByteWriter = (*netConn)(nil)
+var _ ReadWriteCloser = (*Conn)(nil)
diff --git a/lib/fed/middleware.go b/lib/fed/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..c47a3f7043a23b693c72bacff6f43c6898ab283d
--- /dev/null
+++ b/lib/fed/middleware.go
@@ -0,0 +1,7 @@
+package fed
+
+// Middleware intercepts packets and possibly changes them. Return a 0 length packet to cancel.
+type Middleware interface {
+	ReadPacket(packet Packet) (Packet, error)
+	WritePacket(packet Packet) (Packet, error)
+}
diff --git a/lib/fed/middlewares/eqp/client.go b/lib/fed/middlewares/eqp/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..47417442094ac0b94e8304be45c5fb1af67a8cbd
--- /dev/null
+++ b/lib/fed/middlewares/eqp/client.go
@@ -0,0 +1,25 @@
+package eqp
+
+import (
+	"gfx.cafe/gfx/pggat/lib/fed"
+)
+
+type Client struct {
+	state State
+}
+
+func NewClient() *Client {
+	return new(Client)
+}
+
+func (T *Client) ReadPacket(packet fed.Packet) (fed.Packet, error) {
+	T.state.C2S(packet)
+	return packet, nil
+}
+
+func (T *Client) WritePacket(packet fed.Packet) (fed.Packet, error) {
+	T.state.S2C(packet)
+	return packet, nil
+}
+
+var _ fed.Middleware = (*Client)(nil)
diff --git a/lib/fed/middlewares/eqp/server.go b/lib/fed/middlewares/eqp/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..f1b9e7889ac2b5bf3fae74e07edf4e4c73080bcc
--- /dev/null
+++ b/lib/fed/middlewares/eqp/server.go
@@ -0,0 +1,25 @@
+package eqp
+
+import (
+	"gfx.cafe/gfx/pggat/lib/fed"
+)
+
+type Server struct {
+	state State
+}
+
+func NewServer() *Server {
+	return new(Server)
+}
+
+func (T *Server) ReadPacket(packet fed.Packet) (fed.Packet, error) {
+	T.state.S2C(packet)
+	return packet, nil
+}
+
+func (T *Server) WritePacket(packet fed.Packet) (fed.Packet, error) {
+	T.state.C2S(packet)
+	return packet, nil
+}
+
+var _ fed.Middleware = (*Server)(nil)
diff --git a/lib/middleware/middlewares/eqp/state.go b/lib/fed/middlewares/eqp/state.go
similarity index 100%
rename from lib/middleware/middlewares/eqp/state.go
rename to lib/fed/middlewares/eqp/state.go
diff --git a/lib/middleware/middlewares/eqp/sync.go b/lib/fed/middlewares/eqp/sync.go
similarity index 92%
rename from lib/middleware/middlewares/eqp/sync.go
rename to lib/fed/middlewares/eqp/sync.go
index 4bcd2cf58a1c1bc111d5ea7d408e6092d28de4cf..150d8e0d9ef2824a7cbbdc4f9029dc92d29abf7d 100644
--- a/lib/middleware/middlewares/eqp/sync.go
+++ b/lib/fed/middlewares/eqp/sync.go
@@ -6,7 +6,7 @@ import (
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 )
 
-func Sync(c *Client, server fed.ReadWriter, s *Server) error {
+func Sync(c *Client, server *fed.Conn, s *Server) error {
 	var needsBackendSync bool
 
 	// close all portals on server
@@ -83,11 +83,8 @@ func Sync(c *Client, server fed.ReadWriter, s *Server) error {
 	}
 
 	if needsBackendSync {
-		ctx := backends.Context{
-			Packet: packet,
-			Server: server,
-		}
-		_, err := backends.Sync(&ctx)
+		var err error
+		err, _, packet = backends.Sync(server, nil, packet)
 		return err
 	}
 
diff --git a/lib/middleware/middlewares/ps/client.go b/lib/fed/middlewares/ps/client.go
similarity index 69%
rename from lib/middleware/middlewares/ps/client.go
rename to lib/fed/middlewares/ps/client.go
index c0b4fca687d57a1c992325b781eb40f5de025da1..ecb819f992b19c47dbf313604db87df7e0f7b2d9 100644
--- a/lib/middleware/middlewares/ps/client.go
+++ b/lib/fed/middlewares/ps/client.go
@@ -5,7 +5,6 @@ import (
 
 	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
-	"gfx.cafe/gfx/pggat/lib/middleware"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
@@ -20,29 +19,28 @@ func NewClient(parameters map[strutil.CIString]string) *Client {
 	}
 }
 
-func (T *Client) Read(_ middleware.Context, _ fed.Packet) error {
-	return nil
+func (T *Client) ReadPacket(packet fed.Packet) (fed.Packet, error) {
+	return packet, nil
 }
 
-func (T *Client) Write(ctx middleware.Context, packet fed.Packet) error {
+func (T *Client) WritePacket(packet fed.Packet) (fed.Packet, error) {
 	switch packet.Type() {
 	case packets.TypeParameterStatus:
 		var ps packets.ParameterStatus
 		if !ps.ReadFromPacket(packet) {
-			return errors.New("bad packet format i")
+			return packet, errors.New("bad packet format i")
 		}
 		ikey := strutil.MakeCIString(ps.Key)
 		if T.synced && T.parameters[ikey] == ps.Value {
 			// already set
-			ctx.Cancel()
-			break
+			return packet[:0], nil
 		}
 		if T.parameters == nil {
 			T.parameters = make(map[strutil.CIString]string)
 		}
 		T.parameters[ikey] = ps.Value
 	}
-	return nil
+	return packet, nil
 }
 
-var _ middleware.Middleware = (*Client)(nil)
+var _ fed.Middleware = (*Client)(nil)
diff --git a/lib/middleware/middlewares/ps/server.go b/lib/fed/middlewares/ps/server.go
similarity index 68%
rename from lib/middleware/middlewares/ps/server.go
rename to lib/fed/middlewares/ps/server.go
index dc11a6e14d8eeb44d87dec7313859f314cb06afd..f74f4f0c8c2c15414a34524984b83d313cc53159 100644
--- a/lib/middleware/middlewares/ps/server.go
+++ b/lib/fed/middlewares/ps/server.go
@@ -5,7 +5,6 @@ import (
 
 	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
-	"gfx.cafe/gfx/pggat/lib/middleware"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
@@ -19,12 +18,12 @@ func NewServer(parameters map[strutil.CIString]string) *Server {
 	}
 }
 
-func (T *Server) Read(_ middleware.Context, packet fed.Packet) error {
+func (T *Server) ReadPacket(packet fed.Packet) (fed.Packet, error) {
 	switch packet.Type() {
 	case packets.TypeParameterStatus:
 		var ps packets.ParameterStatus
 		if !ps.ReadFromPacket(packet) {
-			return errors.New("bad packet format j")
+			return packet, errors.New("bad packet format j")
 		}
 		ikey := strutil.MakeCIString(ps.Key)
 		if T.parameters == nil {
@@ -32,11 +31,11 @@ func (T *Server) Read(_ middleware.Context, packet fed.Packet) error {
 		}
 		T.parameters[ikey] = ps.Value
 	}
-	return nil
+	return packet, nil
 }
 
-func (T *Server) Write(_ middleware.Context, _ fed.Packet) error {
-	return nil
+func (T *Server) WritePacket(packet fed.Packet) (fed.Packet, error) {
+	return packet, nil
 }
 
-var _ middleware.Middleware = (*Server)(nil)
+var _ fed.Middleware = (*Server)(nil)
diff --git a/lib/middleware/middlewares/ps/sync.go b/lib/fed/middlewares/ps/sync.go
similarity index 76%
rename from lib/middleware/middlewares/ps/sync.go
rename to lib/fed/middlewares/ps/sync.go
index 4910ae52412e127b36f8c3eb54fc63fed4aab973..be296b9c3421570d64d5d46460fdf623419c5325 100644
--- a/lib/middleware/middlewares/ps/sync.go
+++ b/lib/fed/middlewares/ps/sync.go
@@ -8,7 +8,7 @@ import (
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-func sync(tracking []strutil.CIString, client fed.ReadWriter, c *Client, server fed.ReadWriter, s *Server, name strutil.CIString) error {
+func sync(tracking []strutil.CIString, client *fed.Conn, c *Client, server *fed.Conn, s *Server, name strutil.CIString) error {
 	value, hasValue := c.parameters[name]
 	expected, hasExpected := s.parameters[name]
 
@@ -31,14 +31,10 @@ func sync(tracking []strutil.CIString, client fed.ReadWriter, c *Client, server
 	var doSet bool
 
 	if hasValue && slices.Contains(tracking, name) {
-		ctx := backends.Context{
-			Packet: packet,
-			Server: server,
-		}
-		if err := backends.SetParameter(&ctx, name, value); err != nil {
+		var err error
+		if err, _, packet = backends.SetParameter(server, nil, packet, name, value); err != nil {
 			return err
 		}
-		packet = ctx.Packet
 		if s.parameters == nil {
 			s.parameters = make(map[strutil.CIString]string)
 		}
@@ -63,7 +59,7 @@ func sync(tracking []strutil.CIString, client fed.ReadWriter, c *Client, server
 	return nil
 }
 
-func Sync(tracking []strutil.CIString, client fed.ReadWriter, c *Client, server fed.ReadWriter, s *Server) (clientErr, serverErr error) {
+func Sync(tracking []strutil.CIString, client *fed.Conn, c *Client, server *fed.Conn, s *Server) (clientErr, serverErr error) {
 	for name := range c.parameters {
 		if serverErr = sync(tracking, client, c, server, s, name); serverErr != nil {
 			return
diff --git a/lib/middleware/middlewares/unterminate/unterminate.go b/lib/fed/middlewares/unterminate/unterminate.go
similarity index 60%
rename from lib/middleware/middlewares/unterminate/unterminate.go
rename to lib/fed/middlewares/unterminate/unterminate.go
index dcd27cfd12fea58f3c58dd28f742478b7490d98d..f04fb06b5e66dd9556aad67f44255e4aa6d45129 100644
--- a/lib/middleware/middlewares/unterminate/unterminate.go
+++ b/lib/fed/middlewares/unterminate/unterminate.go
@@ -5,7 +5,6 @@ import (
 
 	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
-	"gfx.cafe/gfx/pggat/lib/middleware"
 )
 
 // Unterminate catches the Terminate packet and returns io.EOF instead.
@@ -14,15 +13,15 @@ var Unterminate = unterm{}
 
 type unterm struct{}
 
-func (unterm) Read(_ middleware.Context, packet fed.Packet) error {
+func (unterm) ReadPacket(packet fed.Packet) (fed.Packet, error) {
 	if packet.Type() == packets.TypeTerminate {
-		return io.EOF
+		return packet, io.EOF
 	}
-	return nil
+	return packet, nil
 }
 
-func (unterm) Write(_ middleware.Context, _ fed.Packet) error {
-	return nil
+func (unterm) WritePacket(packet fed.Packet) (fed.Packet, error) {
+	return packet, nil
 }
 
-var _ middleware.Middleware = unterm{}
+var _ fed.Middleware = unterm{}
diff --git a/lib/fed/netconn.go b/lib/fed/netconn.go
new file mode 100644
index 0000000000000000000000000000000000000000..480edb12b2c66cca2eea13888daac31ca226c404
--- /dev/null
+++ b/lib/fed/netconn.go
@@ -0,0 +1,144 @@
+package fed
+
+import (
+	"bufio"
+	"crypto/tls"
+	"encoding/binary"
+	"errors"
+	"io"
+	"net"
+
+	"gfx.cafe/gfx/pggat/lib/util/slices"
+)
+
+type NetConn struct {
+	conn       net.Conn
+	writer     bufio.Writer
+	reader     bufio.Reader
+	sslEnabled bool
+
+	headerBuf [5]byte
+}
+
+func NewNetConn(conn net.Conn) *NetConn {
+	c := &NetConn{
+		conn: conn,
+	}
+	c.writer.Reset(conn)
+	c.reader.Reset(conn)
+	return c
+}
+
+func (T *NetConn) LocalAddr() net.Addr {
+	return T.conn.LocalAddr()
+}
+
+func (T *NetConn) RemoteAddr() net.Addr {
+	return T.conn.RemoteAddr()
+}
+
+// SSL
+
+var errSSLAlreadyEnabled = errors.New("ssl is already enabled")
+
+func (T *NetConn) SSL() bool {
+	return T.sslEnabled
+}
+
+func (T *NetConn) EnableSSLClient(config *tls.Config) error {
+	if T.sslEnabled {
+		return errSSLAlreadyEnabled
+	}
+	T.sslEnabled = true
+
+	if err := T.writer.Flush(); err != nil {
+		return err
+	}
+	if T.reader.Buffered() > 0 {
+		return errors.New("expected empty read buffer")
+	}
+	sslConn := tls.Client(T.conn, config)
+	T.writer.Reset(sslConn)
+	T.reader.Reset(sslConn)
+	T.conn = sslConn
+	return sslConn.Handshake()
+}
+
+func (T *NetConn) EnableSSLServer(config *tls.Config) error {
+	if T.sslEnabled {
+		return errSSLAlreadyEnabled
+	}
+	T.sslEnabled = true
+
+	if err := T.writer.Flush(); err != nil {
+		return err
+	}
+	if T.reader.Buffered() > 0 {
+		return errors.New("expected empty read buffer")
+	}
+	sslConn := tls.Server(T.conn, config)
+	T.writer.Reset(sslConn)
+	T.reader.Reset(sslConn)
+	T.conn = sslConn
+	return sslConn.Handshake()
+}
+
+func (T *NetConn) ReadByte() (byte, error) {
+	if err := T.writer.Flush(); err != nil {
+		return 0, err
+	}
+	return T.reader.ReadByte()
+}
+
+func (T *NetConn) ReadPacket(typed bool, buffer Packet) (packet Packet, err error) {
+	packet = buffer
+
+	if err = T.writer.Flush(); err != nil {
+		return
+	}
+
+	if typed {
+		_, err = io.ReadFull(&T.reader, T.headerBuf[:])
+		if err != nil {
+			return
+		}
+	} else {
+		_, err = io.ReadFull(&T.reader, T.headerBuf[1:])
+		if err != nil {
+			return
+		}
+	}
+
+	length := binary.BigEndian.Uint32(T.headerBuf[1:])
+
+	packet = slices.Resize(buffer, int(length)+1)
+	copy(packet, T.headerBuf[:])
+
+	_, err = io.ReadFull(&T.reader, packet.Payload())
+	if err != nil {
+		return
+	}
+	return
+}
+
+func (T *NetConn) WriteByte(b byte) error {
+	return T.writer.WriteByte(b)
+}
+
+func (T *NetConn) WritePacket(packet Packet) error {
+	_, err := T.writer.Write(packet.Bytes())
+	return err
+}
+
+func (T *NetConn) Close() error {
+	if err := T.writer.Flush(); err != nil {
+		return err
+	}
+	return T.conn.Close()
+}
+
+var _ ReadWriteCloser = (*NetConn)(nil)
+var _ SSLServer = (*NetConn)(nil)
+var _ SSLClient = (*NetConn)(nil)
+var _ io.ByteReader = (*NetConn)(nil)
+var _ io.ByteWriter = (*NetConn)(nil)
diff --git a/lib/fed/readwriter.go b/lib/fed/readwriter.go
index e26d66f933db5e2e3368ace6f4ab398330c98f2e..bde9a0d17f45d0b48b682e4c87d2108b419ff9dd 100644
--- a/lib/fed/readwriter.go
+++ b/lib/fed/readwriter.go
@@ -13,7 +13,8 @@ type ReadWriter interface {
 	Writer
 }
 
-type CombinedReadWriter struct {
-	Reader
-	Writer
+type ReadWriteCloser interface {
+	ReadWriter
+
+	Close() error
 }
diff --git a/lib/fed/ssl.go b/lib/fed/ssl.go
index 580d83f983f4a5696610a2b643a5f8c9dec0a3e2..9b830d23ded6eef3b32461fb91053ec67a3d61ae 100644
--- a/lib/fed/ssl.go
+++ b/lib/fed/ssl.go
@@ -2,10 +2,18 @@ package fed
 
 import "crypto/tls"
 
+type SSL interface {
+	SSL() bool
+}
+
 type SSLClient interface {
+	SSL
+
 	EnableSSLClient(config *tls.Config) error
 }
 
 type SSLServer interface {
+	SSL
+
 	EnableSSLServer(config *tls.Config) error
 }
diff --git a/lib/gat/app.go b/lib/gat/app.go
new file mode 100644
index 0000000000000000000000000000000000000000..2bb1a58f93f78afe42bcb955927bf3b593236394
--- /dev/null
+++ b/lib/gat/app.go
@@ -0,0 +1,106 @@
+package gat
+
+import (
+	"time"
+
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
+
+	"gfx.cafe/gfx/pggat/lib/gat/metrics"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+)
+
+type Config struct {
+	StatLogPeriod dur.Duration   `json:"stat_log_period,omitempty"`
+	Servers       []ServerConfig `json:"servers,omitempty"`
+}
+
+func init() {
+	caddy.RegisterModule((*App)(nil))
+}
+
+type App struct {
+	Config
+
+	servers []*Server
+
+	closed chan struct{}
+
+	log *zap.Logger
+}
+
+func (T *App) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat",
+		New: func() caddy.Module {
+			return new(App)
+		},
+	}
+}
+
+func (T *App) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+
+	T.servers = make([]*Server, 0, len(T.Servers))
+	for _, config := range T.Servers {
+		server := &Server{
+			ServerConfig: config,
+		}
+		if err := server.Provision(ctx); err != nil {
+			return err
+		}
+		T.servers = append(T.servers, server)
+	}
+
+	return nil
+}
+
+func (T *App) statLogLoop() {
+	t := time.NewTicker(T.StatLogPeriod.Duration())
+	defer t.Stop()
+
+	var stats metrics.Server
+	for {
+		select {
+		case <-t.C:
+			for _, server := range T.servers {
+				server.ReadMetrics(&stats)
+			}
+			T.log.Info(stats.String())
+			stats.Clear()
+		case <-T.closed:
+			return
+		}
+	}
+}
+
+func (T *App) Start() error {
+	T.closed = make(chan struct{})
+	if T.StatLogPeriod != 0 {
+		go T.statLogLoop()
+	}
+
+	for _, server := range T.servers {
+		if err := server.Start(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (T *App) Stop() error {
+	close(T.closed)
+
+	for _, server := range T.servers {
+		if err := server.Stop(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+var _ caddy.Module = (*App)(nil)
+var _ caddy.Provisioner = (*App)(nil)
+var _ caddy.App = (*App)(nil)
diff --git a/lib/gat/endpoint.go b/lib/gat/endpoint.go
deleted file mode 100644
index 269f4a8af118222c03efbde114bedf4aaaecae5e..0000000000000000000000000000000000000000
--- a/lib/gat/endpoint.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package gat
-
-import "gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
-
-type FrontendAcceptOptions = frontends.AcceptOptions
-
-type Endpoint struct {
-	Network string
-	Address string
-
-	AcceptOptions FrontendAcceptOptions
-}
diff --git a/lib/gat/gatcaddyfile/README.md b/lib/gat/gatcaddyfile/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4a2141f8dd7c9acc6b39ec238d4f22f2304272be
--- /dev/null
+++ b/lib/gat/gatcaddyfile/README.md
@@ -0,0 +1,9 @@
+## Server Blocks
+Server blocks will be matched in order by their keys. Different SSL configurations may not be used on the same host port pair
+(due to technical limitations).
+
+## Directives
+| Directive | Description                       |
+|-----------|-----------------------------------|
+| ssl       | ssl configuration for this server |
+| {handler} | each handler will be run in order |
diff --git a/lib/gat/gatcaddyfile/discoverer.go b/lib/gat/gatcaddyfile/discoverer.go
new file mode 100644
index 0000000000000000000000000000000000000000..f580483395ffea33442d8b359dfe911e8ceff7d1
--- /dev/null
+++ b/lib/gat/gatcaddyfile/discoverer.go
@@ -0,0 +1,65 @@
+package gatcaddyfile
+
+import (
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery/discoverers/digitalocean"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery/discoverers/google_cloud_sql"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery/discoverers/zalando_operator"
+)
+
+func init() {
+	RegisterDirective(Discoverer, "digitalocean", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+
+		apiKey := d.Val()
+
+		return &digitalocean.Discoverer{
+			Config: digitalocean.Config{
+				APIKey: apiKey,
+			},
+		}, nil
+	})
+	RegisterDirective(Discoverer, "google_cloud_sql", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := google_cloud_sql.Discoverer{
+			Config: google_cloud_sql.Config{
+				IpAddressType: "PRIMARY",
+			},
+		}
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.Project = d.Val()
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.AuthUser = d.Val()
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.AuthPassword = d.Val()
+
+		return &module, nil
+	})
+	RegisterDirective(Discoverer, "zalando_operator", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := zalando_operator.Discoverer{
+			Config: zalando_operator.Config{
+				Namespace: "default",
+			},
+		}
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.OperatorConfigurationObject = d.Val()
+
+		return &module, nil
+	})
+}
diff --git a/lib/gat/gatcaddyfile/gattype.go b/lib/gat/gatcaddyfile/gattype.go
new file mode 100644
index 0000000000000000000000000000000000000000..6800adc05823b9ec542606ab6d411210ff8f3f1b
--- /dev/null
+++ b/lib/gat/gatcaddyfile/gattype.go
@@ -0,0 +1,253 @@
+package gatcaddyfile
+
+import (
+	"encoding/json"
+	"strings"
+	"time"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/matchers"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/self_signed"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+)
+
+func init() {
+	caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
+}
+
+type ServerType struct{}
+
+func (ServerType) Setup(blocks []caddyfile.ServerBlock, m map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
+	var config caddy.Config
+	var warnings []caddyconfig.Warning
+
+	app := gat.App{
+		Config: gat.Config{
+			StatLogPeriod: dur.Duration(1 * time.Minute),
+		},
+	}
+
+	for i, block := range blocks {
+		if i == 0 && len(block.Keys) == 0 {
+			// global options
+			for _, segment := range block.Segments {
+				d := caddyfile.NewDispenser(segment)
+				if !d.Next() {
+					continue
+				}
+				directive := d.Val()
+				switch {
+				case directive == "stat_log_period":
+					if !d.NextArg() {
+						return nil, nil, d.ArgErr()
+					}
+
+					period, err := time.ParseDuration(d.Val())
+					if err != nil {
+						return nil, nil, d.WrapErr(err)
+					}
+
+					app.StatLogPeriod = dur.Duration(period)
+				default:
+					return nil, nil, d.SyntaxErr("global options")
+				}
+
+				if d.CountRemainingArgs() > 0 {
+					return nil, nil, d.ArgErr()
+				}
+			}
+
+			continue
+		}
+
+		var server gat.ServerConfig
+
+		server.Listen = make([]gat.ListenerConfig, 0, len(block.Keys))
+		for _, key := range block.Keys {
+			listen := gat.ListenerConfig{
+				Address: key,
+			}
+			server.Listen = append(server.Listen, listen)
+		}
+
+		var namedMatchers map[string]json.RawMessage
+
+		for _, segment := range block.Segments {
+			d := caddyfile.NewDispenser(segment)
+			if !d.Next() {
+				continue
+			}
+			directive := d.Val()
+			switch {
+			case directive == "ssl":
+				var val json.RawMessage
+				if !d.NextArg() {
+					// self signed ssl
+					val = caddyconfig.JSONModuleObject(
+						self_signed.Server{},
+						"provider",
+						"self_signed",
+						&warnings,
+					)
+				} else {
+					var err error
+					val, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						SSLServer,
+						"provider",
+						&warnings,
+					)
+					if err != nil {
+						return nil, nil, err
+					}
+				}
+
+				// set
+				for i := range server.Listen {
+					if server.Listen[i].SSL != nil {
+						return nil, nil, d.Err("duplicate ssl directive")
+					}
+					server.Listen[i].SSL = val
+				}
+
+				if d.CountRemainingArgs() > 0 {
+					return nil, nil, d.ArgErr()
+				}
+			case strings.HasPrefix(directive, "@"):
+				name := strings.TrimPrefix(directive, "@")
+				if _, ok := namedMatchers[name]; ok {
+					return nil, nil, d.Errf(`duplicate named matcher "%s"`, name)
+				}
+
+				var matcher json.RawMessage
+
+				// read named matcher
+				if d.NextArg() {
+					// inline
+					var err error
+					matcher, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						Matcher,
+						"matcher",
+						&warnings,
+					)
+					if err != nil {
+						return nil, nil, err
+					}
+				} else {
+					// block
+					if !d.NextBlock(0) {
+						return nil, nil, d.ArgErr()
+					}
+
+					var and matchers.And
+
+					for {
+						if d.Val() == "}" {
+							break
+						}
+
+						cond, err := UnmarshalDirectiveJSONModuleObject(
+							d,
+							Matcher,
+							"matcher",
+							&warnings,
+						)
+						if err != nil {
+							return nil, nil, err
+						}
+						and.And = append(and.And, cond)
+
+						if !d.NextLine() {
+							return nil, nil, d.EOFErr()
+						}
+					}
+
+					if len(and.And) == 0 {
+						matcher = nil
+					} else if len(and.And) == 1 {
+						matcher = and.And[0]
+					} else {
+						matcher = caddyconfig.JSONModuleObject(
+							and,
+							Matcher,
+							"matcher",
+							&warnings,
+						)
+					}
+				}
+
+				if d.CountRemainingArgs() > 0 {
+					return nil, nil, d.ArgErr()
+				}
+
+				if namedMatchers == nil {
+					namedMatchers = make(map[string]json.RawMessage)
+				}
+				namedMatchers[name] = matcher
+			default:
+				unmarshaller, ok := LookupDirective(Handler, d.Val())
+				if !ok {
+					return nil, nil, d.Errf(`unknown handler "%s"`, d.Val())
+				}
+
+				var route gat.RouteConfig
+
+				// try to read matcher
+				if d.NextArg() {
+					matcher := d.Val()
+					switch {
+					case strings.HasPrefix(matcher, "@"): // named matcher
+						route.Match, ok = namedMatchers[strings.TrimPrefix(matcher, "@")]
+						if !ok {
+							return nil, nil, d.Errf(`unknown named matcher "%s"`, matcher)
+						}
+					case strings.HasPrefix(matcher, "/"): // database
+						route.Match = caddyconfig.JSONModuleObject(
+							matchers.Database{
+								Database: strings.TrimPrefix(matcher, "/"),
+							},
+							"matcher",
+							"database",
+							&warnings,
+						)
+					case matcher == "*":
+						route.Match = nil // wildcard
+					default:
+						d.Prev()
+					}
+				}
+
+				var err error
+				route.Handle, err = unmarshaller.JSONModuleObject(
+					d,
+					Handler,
+					"handler",
+					&warnings,
+				)
+				if err != nil {
+					return nil, nil, err
+				}
+
+				if d.CountRemainingArgs() > 0 {
+					return nil, nil, d.ArgErr()
+				}
+
+				server.Routes = append(server.Routes, route)
+			}
+		}
+
+		app.Servers = append(app.Servers, server)
+	}
+
+	if config.AppsRaw == nil {
+		config.AppsRaw = make(caddy.ModuleMap)
+	}
+	config.AppsRaw[string(app.CaddyModule().ID)] = caddyconfig.JSON(app, &warnings)
+
+	return &config, warnings, nil
+}
diff --git a/lib/gat/gatcaddyfile/handler.go b/lib/gat/gatcaddyfile/handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..758478728e635faee8bf397e87390a27334d2cbf
--- /dev/null
+++ b/lib/gat/gatcaddyfile/handler.go
@@ -0,0 +1,518 @@
+package gatcaddyfile
+
+import (
+	"strconv"
+	"strings"
+	"time"
+
+	error_handler "gfx.cafe/gfx/pggat/lib/gat/handlers/error"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer_spilo"
+	pool_handler "gfx.cafe/gfx/pggat/lib/gat/handlers/pool"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/bouncer"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/allowed_startup_parameters"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/require_ssl"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_database"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_parameter"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_password"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_user"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/transaction"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/clients/insecure_skip_verify"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+func init() {
+	RegisterDirective(Handler, "require_ssl", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		var ssl = true
+		if d.NextArg() {
+			switch d.Val() {
+			case "true":
+				ssl = true
+			case "false":
+				ssl = false
+			default:
+				return nil, d.SyntaxErr("boolean")
+			}
+		}
+		return &require_ssl.Module{
+			SSL: ssl,
+		}, nil
+	})
+	RegisterDirective(Handler, "allowed_startup_parameters", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextBlock(d.Nesting()) {
+			return nil, d.ArgErr()
+		}
+
+		var module allowed_startup_parameters.Module
+
+		for {
+			if d.Val() == "}" {
+				break
+			}
+
+			module.Parameters = append(module.Parameters, strutil.MakeCIString(d.Val()))
+
+			if !d.NextLine() {
+				return nil, d.EOFErr()
+			}
+		}
+
+		return &module, nil
+	})
+	RegisterDirective(Handler, "user", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+
+		return &rewrite_user.Module{
+			User: d.Val(),
+		}, nil
+	})
+	RegisterDirective(Handler, "password", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+
+		return &rewrite_password.Module{
+			Password: d.Val(),
+		}, nil
+	})
+	RegisterDirective(Handler, "database", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+
+		return &rewrite_database.Module{
+			Database: d.Val(),
+		}, nil
+	})
+	RegisterDirective(Handler, "parameter", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+
+		keyValue := d.Val()
+		key, value, ok := strings.Cut(keyValue, "=")
+		if !ok {
+			return nil, d.SyntaxErr("key=value")
+		}
+
+		return &rewrite_parameter.Module{
+			Key:   strutil.MakeCIString(key),
+			Value: value,
+		}, nil
+	})
+	RegisterDirective(Handler, "error", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+
+		message := d.Val()
+
+		return &error_handler.Module{
+			Message: message,
+		}, nil
+	})
+	RegisterDirective(Handler, "pgbouncer", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		var config = "pgbouncer.ini"
+		if d.NextArg() {
+			config = d.Val()
+		}
+		return &pgbouncer.Module{
+			ConfigFile: config,
+		}, nil
+	})
+	RegisterDirective(Handler, "discovery", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := discovery.Module{
+			Config: discovery.Config{
+				ReconcilePeriod: dur.Duration(5 * time.Minute),
+				Pooler: JSONModuleObject(
+					&transaction.Module{
+						ManagementConfig: defaultPoolManagementConfig,
+					},
+					Pooler,
+					"pooler",
+					warnings,
+				),
+				ServerSSLMode: bouncer.SSLModePrefer,
+				ServerSSL: JSONModuleObject(
+					&insecure_skip_verify.Client{},
+					SSLClient,
+					"provider",
+					warnings,
+				),
+			},
+		}
+
+		if d.NextArg() {
+			// discoverer
+			var err error
+			module.Discoverer, err = UnmarshalDirectiveJSONModuleObject(
+				d,
+				Discoverer,
+				"discoverer",
+				warnings,
+			)
+
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			if !d.NextBlock(d.Nesting()) {
+				return nil, d.ArgErr()
+			}
+
+			for {
+				if d.Val() == "}" {
+					break
+				}
+
+				directive := d.Val()
+				switch directive {
+				case "reconcile_period":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					val, err := time.ParseDuration(d.Val())
+					if err != nil {
+						return nil, d.WrapErr(err)
+					}
+					module.ReconcilePeriod = dur.Duration(val)
+				case "discoverer":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					var err error
+					module.Discoverer, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						Discoverer,
+						"discoverer",
+						warnings,
+					)
+					if err != nil {
+						return nil, err
+					}
+				case "pooler":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					var err error
+					module.Pooler, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						Pooler,
+						"pooler",
+						warnings,
+					)
+					if err != nil {
+						return nil, err
+					}
+				case "ssl":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					module.ServerSSLMode = bouncer.SSLMode(d.Val())
+
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					var err error
+					module.ServerSSL, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						SSLClient,
+						"provider",
+						warnings,
+					)
+					if err != nil {
+						return nil, err
+					}
+				case "parameter":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					keyValue := d.Val()
+					key, value, ok := strings.Cut(keyValue, "=")
+					if !ok {
+						return nil, d.SyntaxErr("key=value")
+					}
+					if module.ServerStartupParameters == nil {
+						module.ServerStartupParameters = make(map[string]string)
+					}
+					module.ServerStartupParameters[key] = value
+				default:
+					return nil, d.ArgErr()
+				}
+
+				if !d.NextLine() {
+					return nil, d.EOFErr()
+				}
+			}
+		}
+
+		return &module, nil
+	})
+	RegisterDirective(Handler, "pgbouncer_spilo", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := pgbouncer_spilo.Module{}
+
+		if !d.NextBlock(d.Nesting()) {
+			return nil, d.ArgErr()
+		}
+
+		for {
+			if d.Val() == "}" {
+				break
+			}
+
+			directive := d.Val()
+			switch directive {
+			case "host":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				module.Host = d.Val()
+			case "port":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				var err error
+				module.Port, err = strconv.Atoi(d.Val())
+				if err != nil {
+					return nil, d.WrapErr(err)
+				}
+			case "user":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				module.User = d.Val()
+			case "schema":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				module.Schema = d.Val()
+			case "password":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				module.Password = d.Val()
+			case "mode":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				module.Mode = d.Val()
+			case "default_size":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				var err error
+				module.DefaultSize, err = strconv.Atoi(d.Val())
+				if err != nil {
+					return nil, d.WrapErr(err)
+				}
+			case "min_size":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				var err error
+				module.MinSize, err = strconv.Atoi(d.Val())
+				if err != nil {
+					return nil, d.WrapErr(err)
+				}
+			case "reserve_size":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				var err error
+				module.ReserveSize, err = strconv.Atoi(d.Val())
+				if err != nil {
+					return nil, d.WrapErr(err)
+				}
+			case "max_client_conn":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				var err error
+				module.MaxClientConn, err = strconv.Atoi(d.Val())
+				if err != nil {
+					return nil, d.WrapErr(err)
+				}
+			case "max_db_conn":
+				if !d.NextArg() {
+					return nil, d.ArgErr()
+				}
+
+				var err error
+				module.MaxDBConn, err = strconv.Atoi(d.Val())
+				if err != nil {
+					return nil, d.WrapErr(err)
+				}
+			default:
+				return nil, d.ArgErr()
+			}
+
+			if !d.NextLine() {
+				return nil, d.EOFErr()
+			}
+		}
+
+		return &module, nil
+	})
+	RegisterDirective(Handler, "pool", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := pool_handler.Module{
+			Config: pool_handler.Config{
+				Pooler: JSONModuleObject(
+					&transaction.Module{
+						ManagementConfig: defaultPoolManagementConfig,
+					},
+					Pooler,
+					"pooler",
+					warnings,
+				),
+
+				ServerSSLMode: bouncer.SSLModePrefer,
+				ServerSSL: JSONModuleObject(
+					&insecure_skip_verify.Client{},
+					SSLClient,
+					"provider",
+					warnings,
+				),
+			},
+		}
+
+		if d.NextArg() {
+			module.ServerAddress = d.Val()
+
+			if !d.NextArg() {
+				return nil, d.ArgErr()
+			}
+			module.ServerDatabase = d.Val()
+
+			if !d.NextArg() {
+				return nil, d.ArgErr()
+			}
+			module.ServerUsername = d.Val()
+
+			if !d.NextArg() {
+				return nil, d.ArgErr()
+			}
+			module.ServerPassword = d.Val()
+		} else {
+			if !d.NextBlock(d.Nesting()) {
+				return nil, d.ArgErr()
+			}
+
+			for {
+				if d.Val() == "}" {
+					break
+				}
+
+				directive := d.Val()
+				switch directive {
+				case "pooler":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					var err error
+					module.Pooler, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						Pooler,
+						"pooler",
+						warnings,
+					)
+					if err != nil {
+						return nil, err
+					}
+				case "address":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					module.ServerAddress = d.Val()
+				case "ssl":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					module.ServerSSLMode = bouncer.SSLMode(d.Val())
+
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					var err error
+					module.ServerSSL, err = UnmarshalDirectiveJSONModuleObject(
+						d,
+						SSLClient,
+						"provider",
+						warnings,
+					)
+					if err != nil {
+						return nil, err
+					}
+				case "username":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					module.ServerUsername = d.Val()
+				case "password":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					module.ServerPassword = d.Val()
+				case "database":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					module.ServerDatabase = d.Val()
+				case "parameter":
+					if !d.NextArg() {
+						return nil, d.ArgErr()
+					}
+
+					keyValue := d.Val()
+					key, value, ok := strings.Cut(keyValue, "=")
+					if !ok {
+						return nil, d.SyntaxErr("key=value")
+					}
+					if module.ServerStartupParameters == nil {
+						module.ServerStartupParameters = make(map[string]string)
+					}
+					module.ServerStartupParameters[key] = value
+				default:
+					return nil, d.ArgErr()
+				}
+
+				if !d.NextLine() {
+					return nil, d.EOFErr()
+				}
+			}
+		}
+
+		return &module, nil
+	})
+}
diff --git a/lib/gat/gatcaddyfile/matcher.go b/lib/gat/gatcaddyfile/matcher.go
new file mode 100644
index 0000000000000000000000000000000000000000..07e6822f6b2f350fb92f0ac5bb0cf420e49302a1
--- /dev/null
+++ b/lib/gat/gatcaddyfile/matcher.go
@@ -0,0 +1,194 @@
+package gatcaddyfile
+
+import (
+	"encoding/json"
+	"strings"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/gat/matchers"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+func init() {
+	RegisterDirective(Matcher, "user", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		user := d.Val()
+		return &matchers.User{
+			User: user,
+		}, nil
+	})
+	RegisterDirective(Matcher, "database", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		database := d.Val()
+		return &matchers.Database{
+			Database: database,
+		}, nil
+	})
+	RegisterDirective(Matcher, "local_address", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		address := d.Val()
+		var network string
+		if strings.HasPrefix(address, "/") {
+			network = "unix"
+		} else {
+			network = "tcp"
+		}
+		return &matchers.LocalAddress{
+			Network: network,
+			Address: address,
+		}, nil
+	})
+	RegisterDirective(Matcher, "parameter", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		keyValue := d.Val()
+		key, value, ok := strings.Cut(keyValue, "=")
+		if !ok {
+			return nil, d.SyntaxErr("key=value")
+		}
+		return &matchers.StartupParameter{
+			Key:   strutil.MakeCIString(key),
+			Value: value,
+		}, nil
+	})
+	RegisterDirective(Matcher, "ssl", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		var ssl = true
+		if d.NextArg() {
+			val := d.Val()
+			switch val {
+			case "true":
+				ssl = true
+			case "false":
+				ssl = false
+			default:
+				return nil, d.SyntaxErr("boolean")
+			}
+		}
+
+		return &matchers.SSL{
+			SSL: ssl,
+		}, nil
+	})
+	RegisterDirective(Matcher, "not", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextArg() {
+			return nil, d.SyntaxErr("matcher directive")
+		}
+
+		matcher, err := UnmarshalDirectiveJSONModuleObject(
+			d,
+			Matcher,
+			"matcher",
+			warnings,
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		return &matchers.Not{
+			Not: matcher,
+		}, nil
+	})
+	RegisterDirective(Matcher, "and", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextBlock(d.Nesting()) {
+			return nil, d.ArgErr()
+		}
+
+		var and []caddy.Module
+
+		for {
+			if d.Val() == "}" {
+				break
+			}
+
+			unmarshaller, ok := LookupDirective(Matcher, d.Val())
+			if !ok {
+				return nil, d.Errf(`unknown matcher "%s"`, d.Val())
+			}
+
+			val, err := unmarshaller(d, warnings)
+			if err != nil {
+				return nil, err
+			}
+			and = append(and, val)
+
+			if !d.NextLine() {
+				return nil, d.EOFErr()
+			}
+		}
+
+		if len(and) == 0 {
+			return nil, nil
+		}
+		if len(and) == 1 {
+			return and[0], nil
+		}
+
+		var raw = make([]json.RawMessage, 0, len(and))
+		for _, val := range and {
+			raw = append(raw, JSONModuleObject(
+				val,
+				Matcher,
+				"matcher",
+				warnings,
+			))
+		}
+		return &matchers.And{
+			And: raw,
+		}, nil
+	})
+	RegisterDirective(Matcher, "or", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		if !d.NextBlock(d.Nesting()) {
+			return nil, d.ArgErr()
+		}
+
+		var or []caddy.Module
+
+		for {
+			if d.Val() == "}" {
+				break
+			}
+
+			unmarshaller, ok := LookupDirective(Matcher, d.Val())
+			if !ok {
+				return nil, d.Errf(`unknown matcher "%s"`, d.Val())
+			}
+
+			val, err := unmarshaller(d, warnings)
+			if err != nil {
+				return nil, err
+			}
+			or = append(or, val)
+
+			if !d.NextLine() {
+				return nil, d.EOFErr()
+			}
+		}
+
+		if len(or) == 1 {
+			return or[0], nil
+		}
+
+		var raw = make([]json.RawMessage, 0, len(or))
+		for _, val := range or {
+			raw = append(raw, JSONModuleObject(
+				val,
+				Matcher,
+				"matcher",
+				warnings,
+			))
+		}
+		return &matchers.Or{
+			Or: raw,
+		}, nil
+	})
+}
diff --git a/lib/gat/gatcaddyfile/pooler.go b/lib/gat/gatcaddyfile/pooler.go
new file mode 100644
index 0000000000000000000000000000000000000000..e5e950968c1387b6a3e81cae49d4a3527294893b
--- /dev/null
+++ b/lib/gat/gatcaddyfile/pooler.go
@@ -0,0 +1,115 @@
+package gatcaddyfile
+
+import (
+	"time"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/gat/pool"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/session"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/transaction"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+var defaultPoolManagementConfig = pool.ManagementConfig{
+	ServerIdleTimeout:          dur.Duration(5 * time.Minute),
+	ServerReconnectInitialTime: dur.Duration(5 * time.Second),
+	ServerReconnectMaxTime:     dur.Duration(1 * time.Minute),
+	TrackedParameters: []strutil.CIString{
+		strutil.MakeCIString("client_encoding"),
+		strutil.MakeCIString("datestyle"),
+		strutil.MakeCIString("timezone"),
+		strutil.MakeCIString("standard_conforming_strings"),
+		strutil.MakeCIString("application_name"),
+	},
+}
+
+func unmarshalPoolConfig(d *caddyfile.Dispenser) (pool.ManagementConfig, error) {
+	var config = defaultPoolManagementConfig
+
+	if !d.NextBlock(d.Nesting()) {
+		return config, nil
+	}
+
+	config.TrackedParameters = nil
+
+	for {
+		if d.Val() == "}" {
+			break
+		}
+
+		directive := d.Val()
+		switch directive {
+		case "idle_timeout":
+			if !d.NextArg() {
+				return config, d.ArgErr()
+			}
+
+			val, err := time.ParseDuration(d.Val())
+			if err != nil {
+				return config, d.WrapErr(err)
+			}
+			config.ServerIdleTimeout = dur.Duration(val)
+		case "reconnect":
+			if !d.NextArg() {
+				return config, d.ArgErr()
+			}
+
+			initialTime, err := time.ParseDuration(d.Val())
+			if err != nil {
+				return config, d.WrapErr(err)
+			}
+
+			maxTime := initialTime
+			if d.NextArg() {
+				maxTime, err = time.ParseDuration(d.Val())
+				if err != nil {
+					return config, d.WrapErr(err)
+				}
+			}
+
+			config.ServerReconnectInitialTime = dur.Duration(initialTime)
+			config.ServerReconnectMaxTime = dur.Duration(maxTime)
+		case "track":
+			if !d.NextArg() {
+				return config, d.ArgErr()
+			}
+
+			config.TrackedParameters = append(config.TrackedParameters, strutil.MakeCIString(d.Val()))
+		default:
+			return config, d.ArgErr()
+		}
+
+		if !d.NextLine() {
+			return config, d.EOFErr()
+		}
+	}
+
+	return config, nil
+}
+
+func init() {
+	RegisterDirective(Pooler, "transaction", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		config, err := unmarshalPoolConfig(d)
+		if err != nil {
+			return nil, err
+		}
+
+		return &transaction.Module{
+			ManagementConfig: config,
+		}, nil
+	})
+	RegisterDirective(Pooler, "session", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		config, err := unmarshalPoolConfig(d)
+		if err != nil {
+			return nil, err
+		}
+
+		return &session.Module{
+			ManagementConfig: config,
+		}, nil
+	})
+}
diff --git a/lib/gat/gatcaddyfile/ssl.go b/lib/gat/gatcaddyfile/ssl.go
new file mode 100644
index 0000000000000000000000000000000000000000..a4ae1063ea5c9ad340e212b9dd22972d23629dc5
--- /dev/null
+++ b/lib/gat/gatcaddyfile/ssl.go
@@ -0,0 +1,36 @@
+package gatcaddyfile
+
+import (
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/clients/insecure_skip_verify"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/self_signed"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/x509_key_pair"
+)
+
+func init() {
+	RegisterDirective(SSLServer, "self_signed", func(_ *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		return &self_signed.Server{}, nil
+	})
+	RegisterDirective(SSLServer, "x509_key_pair", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		var module x509_key_pair.Server
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.CertFile = d.Val()
+
+		if !d.NextArg() {
+			return nil, d.ArgErr()
+		}
+		module.KeyFile = d.Val()
+
+		return &module, nil
+	})
+
+	RegisterDirective(SSLClient, "insecure_skip_verify", func(_ *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
+		return &insecure_skip_verify.Client{}, nil
+	})
+}
diff --git a/lib/gat/gatcaddyfile/unmarshaller.go b/lib/gat/gatcaddyfile/unmarshaller.go
new file mode 100644
index 0000000000000000000000000000000000000000..e75b67241eabbb60cd0afda77097bcc87492e540
--- /dev/null
+++ b/lib/gat/gatcaddyfile/unmarshaller.go
@@ -0,0 +1,58 @@
+package gatcaddyfile
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+)
+
+type Unmarshaller func(*caddyfile.Dispenser, *[]caddyconfig.Warning) (caddy.Module, error)
+
+func (T Unmarshaller) JSONModuleObject(
+	d *caddyfile.Dispenser,
+	namespace string,
+	inlineKey string,
+	warnings *[]caddyconfig.Warning,
+) (json.RawMessage, error) {
+	module, err := T(d, warnings)
+	if err != nil {
+		return nil, err
+	}
+
+	return JSONModuleObject(
+		module,
+		namespace,
+		inlineKey,
+		warnings,
+	), nil
+}
+
+func JSONModuleObject(
+	module caddy.Module,
+	namespace string,
+	inlineKey string,
+	warnings *[]caddyconfig.Warning,
+) json.RawMessage {
+	rawModuleID := string(module.CaddyModule().ID)
+	dotModuleID := strings.TrimPrefix(rawModuleID, namespace)
+	moduleID := strings.TrimPrefix(dotModuleID, ".")
+	if rawModuleID == dotModuleID || dotModuleID == moduleID {
+		if warnings != nil {
+			*warnings = append(*warnings, caddyconfig.Warning{
+				Message: fmt.Sprintf(`expected item in namespace "%s" but got "%s"`, namespace, rawModuleID),
+			})
+		}
+		return nil
+	}
+
+	return caddyconfig.JSONModuleObject(
+		module,
+		inlineKey,
+		moduleID,
+		warnings,
+	)
+}
diff --git a/lib/gat/gatcaddyfile/unmarshallers.go b/lib/gat/gatcaddyfile/unmarshallers.go
new file mode 100644
index 0000000000000000000000000000000000000000..0d1f0cd75e45ff374721313958d0c3f431122d36
--- /dev/null
+++ b/lib/gat/gatcaddyfile/unmarshallers.go
@@ -0,0 +1,52 @@
+package gatcaddyfile
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+	"gfx.cafe/gfx/pggat/lib/util/maps"
+)
+
+const (
+	Discoverer = "pggat.handlers.discovery.discoverers"
+	Handler    = "pggat.handlers"
+	Matcher    = "pggat.matchers"
+	Pooler     = "pggat.poolers"
+	SSLServer  = "pggat.ssl.servers"
+	SSLClient  = "pggat.ssl.clients"
+)
+
+var unmarshallers maps.TwoKey[string, string, Unmarshaller]
+
+func RegisterDirective(namespace, directive string, unmarshaller Unmarshaller) {
+	if _, ok := unmarshallers.Load(namespace, directive); ok {
+		panic(fmt.Sprintf(`directive "%s" already exists`, directive))
+	}
+	unmarshallers.Store(namespace, directive, unmarshaller)
+}
+
+func LookupDirective(namespace, directive string) (Unmarshaller, bool) {
+	return unmarshallers.Load(namespace, directive)
+}
+
+func UnmarshalDirectiveJSONModuleObject(
+	d *caddyfile.Dispenser,
+	namespace string,
+	inlineKey string,
+	warnings *[]caddyconfig.Warning,
+) (json.RawMessage, error) {
+	unmarshaller, ok := LookupDirective(namespace, d.Val())
+	if !ok {
+		return nil, d.Errf(`unknown directive in %s: "%s"`, namespace, d.Val())
+	}
+
+	return unmarshaller.JSONModuleObject(
+		d,
+		namespace,
+		inlineKey,
+		warnings,
+	)
+}
diff --git a/lib/gat/handler.go b/lib/gat/handler.go
new file mode 100644
index 0000000000000000000000000000000000000000..6026c1e6778260a9f25d5a2ce66e5b14677c051f
--- /dev/null
+++ b/lib/gat/handler.go
@@ -0,0 +1,25 @@
+package gat
+
+import (
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat/metrics"
+)
+
+// Handler handles the Conn
+type Handler interface {
+	// Handle will attempt to handle the Conn. Return io.EOF for normal disconnection or nil to continue to the next
+	// handle. The error will be relayed to the client so there is no need to send it yourself.
+	Handle(conn *fed.Conn) error
+}
+
+type CancellableHandler interface {
+	Handler
+
+	Cancel(key [8]byte)
+}
+
+type MetricsHandler interface {
+	Handler
+
+	ReadMetrics(metrics *metrics.Handler)
+}
diff --git a/lib/gat/handlers/allowed_startup_parameters/module.go b/lib/gat/handlers/allowed_startup_parameters/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..f23ab20e767f24769ecd06b515263126b18c929b
--- /dev/null
+++ b/lib/gat/handlers/allowed_startup_parameters/module.go
@@ -0,0 +1,47 @@
+package allowed_startup_parameters
+
+import (
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/perror"
+	"gfx.cafe/gfx/pggat/lib/util/slices"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Parameters []strutil.CIString `json:"parameters"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.allowed_startup_parameters",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	for parameter := range conn.InitialParameters {
+		if !slices.Contains(T.Parameters, parameter) {
+			return perror.New(
+				perror.FATAL,
+				perror.FeatureNotSupported,
+				fmt.Sprintf(`Startup parameter "%s" is not supported`, parameter.String()),
+			)
+		}
+	}
+
+	return nil
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/modules/discovery/cluster.go b/lib/gat/handlers/discovery/cluster.go
similarity index 100%
rename from lib/gat/modules/discovery/cluster.go
rename to lib/gat/handlers/discovery/cluster.go
diff --git a/lib/gat/handlers/discovery/config.go b/lib/gat/handlers/discovery/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..b2712561ddabae7281b277222057eb05be92581d
--- /dev/null
+++ b/lib/gat/handlers/discovery/config.go
@@ -0,0 +1,22 @@
+package discovery
+
+import (
+	"encoding/json"
+
+	"gfx.cafe/gfx/pggat/lib/bouncer"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+)
+
+type Config struct {
+	// ReconcilePeriod is how often the module should check for changes. 0 = disable
+	ReconcilePeriod dur.Duration `json:"reconcile_period"`
+
+	Discoverer json.RawMessage `json:"discoverer" caddy:"namespace=pggat.handlers.discovery.discoverers inline_key=discoverer"`
+
+	Pooler json.RawMessage `json:"pooler" caddy:"namespace=pggat.poolers inline_key=pooler"`
+
+	ServerSSLMode bouncer.SSLMode `json:"server_ssl_mode,omitempty"`
+	ServerSSL     json.RawMessage `json:"server_ssl,omitempty" caddy:"namespace=pggat.ssl.clients inline_key=provider"`
+
+	ServerStartupParameters map[string]string `json:"server_startup_parameters,omitempty"`
+}
diff --git a/lib/gat/modules/discovery/discoverer.go b/lib/gat/handlers/discovery/discoverer.go
similarity index 60%
rename from lib/gat/modules/discovery/discoverer.go
rename to lib/gat/handlers/discovery/discoverer.go
index 6ef7581783e1776fe3fd5f32c493d0aaa32b72f7..6fb8373387ae7640390735ecca3a28ba55312d54 100644
--- a/lib/gat/modules/discovery/discoverer.go
+++ b/lib/gat/handlers/discovery/discoverer.go
@@ -1,11 +1,10 @@
 package discovery
 
-// Discoverer looks up and returns the servers. It must implement either Clusters or Added, Updated, and Removed.
-// Both can be implemented for extra robustness.
+// Discoverer looks up and returns the servers. It must implement Clusters. Optionally, it can implement Added
+// and Removed for faster updating. For updates, just send to Added.
 type Discoverer interface {
 	Clusters() ([]Cluster, error)
 
 	Added() <-chan Cluster
-	Updated() <-chan Cluster
 	Removed() <-chan string
 }
diff --git a/lib/gat/handlers/discovery/discoverers/digitalocean/config.go b/lib/gat/handlers/discovery/discoverers/digitalocean/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..ed367390677db01a922ca33afb0017fcf3b1ade5
--- /dev/null
+++ b/lib/gat/handlers/discovery/discoverers/digitalocean/config.go
@@ -0,0 +1,6 @@
+package digitalocean
+
+type Config struct {
+	APIKey  string `json:"api_key"`
+	Private bool   `json:"private"`
+}
diff --git a/lib/gat/modules/digitalocean_discovery/discoverer.go b/lib/gat/handlers/discovery/discoverers/digitalocean/discoverer.go
similarity index 70%
rename from lib/gat/modules/digitalocean_discovery/discoverer.go
rename to lib/gat/handlers/discovery/discoverers/digitalocean/discoverer.go
index 1e4009b013cd3fee56c2c4868ed6815722598c3b..bcafe2c4286f1a7ecfb6dca60fc0d05449c593f1 100644
--- a/lib/gat/modules/digitalocean_discovery/discoverer.go
+++ b/lib/gat/handlers/discovery/discoverers/digitalocean/discoverer.go
@@ -1,29 +1,41 @@
-package digitalocean_discovery
+package digitalocean
 
 import (
 	"context"
 	"net"
 	"strconv"
 
+	"github.com/caddyserver/caddy/v2"
 	"github.com/digitalocean/godo"
 
-	"gfx.cafe/gfx/pggat/lib/gat/modules/discovery"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery"
 )
 
+func init() {
+	caddy.RegisterModule((*Discoverer)(nil))
+}
+
 type Discoverer struct {
-	config Config
+	Config
 
 	do *godo.Client
 }
 
-func NewDiscoverer(config Config) (*Discoverer, error) {
-	return &Discoverer{
-		config: config,
-		do:     godo.NewFromToken(config.APIKey),
-	}, nil
+func (T *Discoverer) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.discovery.discoverers.digitalocean",
+		New: func() caddy.Module {
+			return new(Discoverer)
+		},
+	}
 }
 
-func (T Discoverer) Clusters() ([]discovery.Cluster, error) {
+func (T *Discoverer) Provision(ctx caddy.Context) error {
+	T.do = godo.NewFromToken(T.APIKey)
+	return nil
+}
+
+func (T *Discoverer) Clusters() ([]discovery.Cluster, error) {
 	clusters, _, err := T.do.Databases.List(context.Background(), nil)
 	if err != nil {
 		return nil, err
@@ -36,7 +48,7 @@ func (T Discoverer) Clusters() ([]discovery.Cluster, error) {
 		}
 
 		var primaryAddr string
-		if T.config.Private {
+		if T.Private {
 			primaryAddr = net.JoinHostPort(cluster.PrivateConnection.Host, strconv.Itoa(cluster.PrivateConnection.Port))
 		} else {
 			primaryAddr = net.JoinHostPort(cluster.Connection.Host, strconv.Itoa(cluster.Connection.Port))
@@ -67,7 +79,7 @@ func (T Discoverer) Clusters() ([]discovery.Cluster, error) {
 		c.Replicas = make(map[string]discovery.Endpoint, len(replicas))
 		for _, replica := range replicas {
 			var replicaAddr string
-			if T.config.Private {
+			if T.Private {
 				replicaAddr = net.JoinHostPort(replica.PrivateConnection.Host, strconv.Itoa(replica.PrivateConnection.Port))
 			} else {
 				replicaAddr = net.JoinHostPort(replica.Connection.Host, strconv.Itoa(replica.Connection.Port))
@@ -84,16 +96,13 @@ func (T Discoverer) Clusters() ([]discovery.Cluster, error) {
 	return res, nil
 }
 
-func (T Discoverer) Added() <-chan discovery.Cluster {
-	return nil
-}
-
-func (T Discoverer) Updated() <-chan discovery.Cluster {
+func (T *Discoverer) Added() <-chan discovery.Cluster {
 	return nil
 }
 
-func (T Discoverer) Removed() <-chan string {
+func (T *Discoverer) Removed() <-chan string {
 	return nil
 }
 
 var _ discovery.Discoverer = (*Discoverer)(nil)
+var _ caddy.Module = (*Discoverer)(nil)
diff --git a/lib/gat/handlers/discovery/discoverers/google_cloud_sql/config.go b/lib/gat/handlers/discovery/discoverers/google_cloud_sql/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..00c664fc4c5fa412a4adb874e616e3372e0861ed
--- /dev/null
+++ b/lib/gat/handlers/discovery/discoverers/google_cloud_sql/config.go
@@ -0,0 +1,8 @@
+package google_cloud_sql
+
+type Config struct {
+	Project       string `json:"project"`
+	IpAddressType string `json:"ip_address_type"`
+	AuthUser      string `json:"auth_user"`
+	AuthPassword  string `json:"auth_password"`
+}
diff --git a/lib/gat/modules/cloud_sql_discovery/discoverer.go b/lib/gat/handlers/discovery/discoverers/google_cloud_sql/discoverer.go
similarity index 71%
rename from lib/gat/modules/cloud_sql_discovery/discoverer.go
rename to lib/gat/handlers/discovery/discoverers/google_cloud_sql/discoverer.go
index da7b6b44a9de0ad41a26f7d0723893fb09bbad2f..6bb9351b023cd1c14533778c68fe9c1a575b239d 100644
--- a/lib/gat/modules/cloud_sql_discovery/discoverer.go
+++ b/lib/gat/handlers/discovery/discoverers/google_cloud_sql/discoverer.go
@@ -1,19 +1,21 @@
-package cloud_sql_discovery
+package google_cloud_sql
 
 import (
-	"context"
 	"crypto/tls"
 	"net"
 	"strings"
 
+	"gfx.cafe/gfx/pggat/lib/gat/pool/recipe"
+
+	"github.com/caddyserver/caddy/v2"
 	sqladmin "google.golang.org/api/sqladmin/v1beta4"
 
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery"
+
 	"gfx.cafe/gfx/pggat/lib/auth/credentials"
 	"gfx.cafe/gfx/pggat/lib/bouncer"
-	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
 	"gfx.cafe/gfx/pggat/lib/bouncer/bouncers/v2"
 	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/discovery"
 	"gfx.cafe/gfx/pggat/lib/gsql"
 )
 
@@ -22,28 +24,39 @@ type authQueryResult struct {
 	Password string `sql:"1"`
 }
 
+func init() {
+	caddy.RegisterModule((*Discoverer)(nil))
+}
+
 type Discoverer struct {
-	config Config
+	Config
 
 	google *sqladmin.Service
 }
 
-func NewDiscoverer(config Config) (*Discoverer, error) {
-	google, err := sqladmin.NewService(context.Background())
+func (T *Discoverer) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.discovery.discoverers.google_cloud_sql",
+		New: func() caddy.Module {
+			return new(Discoverer)
+		},
+	}
+}
+
+func (T *Discoverer) Provision(ctx caddy.Context) error {
+	var err error
+	T.google, err = sqladmin.NewService(ctx)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	return &Discoverer{
-		config: config,
-		google: google,
-	}, nil
+	return nil
 }
 
 func (T *Discoverer) instanceToCluster(primary *sqladmin.DatabaseInstance, replicas ...*sqladmin.DatabaseInstance) (discovery.Cluster, error) {
 	var primaryAddress string
 	for _, ip := range primary.IpAddresses {
-		if ip.Type != T.config.IpAddressType {
+		if ip.Type != T.IpAddressType {
 			continue
 		}
 		primaryAddress = net.JoinHostPort(ip.IpAddress, "5432")
@@ -61,7 +74,7 @@ func (T *Discoverer) instanceToCluster(primary *sqladmin.DatabaseInstance, repli
 	for _, replica := range replicas {
 		var replicaAddress string
 		for _, ip := range primary.IpAddresses {
-			if ip.Type != T.config.IpAddressType {
+			if ip.Type != T.IpAddressType {
 				continue
 			}
 			replicaAddress = net.JoinHostPort(ip.IpAddress, "5432")
@@ -72,7 +85,7 @@ func (T *Discoverer) instanceToCluster(primary *sqladmin.DatabaseInstance, repli
 		}
 	}
 
-	databases, err := T.google.Databases.List(T.config.Project, primary.Name).Do()
+	databases, err := T.google.Databases.List(T.Project, primary.Name).Do()
 	if err != nil {
 		return discovery.Cluster{}, err
 	}
@@ -85,42 +98,36 @@ func (T *Discoverer) instanceToCluster(primary *sqladmin.DatabaseInstance, repli
 		return c, nil
 	}
 
-	var admin fed.Conn
+	var admin *fed.Conn
 	defer func() {
 		if admin != nil {
 			_ = admin.Close()
 		}
 	}()
 
-	users, err := T.google.Users.List(T.config.Project, primary.Name).Do()
+	users, err := T.google.Users.List(T.Project, primary.Name).Do()
 	if err != nil {
 		return discovery.Cluster{}, err
 	}
 	c.Users = make([]discovery.User, 0, len(users.Items))
 	for _, user := range users.Items {
 		var password string
-		if user.Name == T.config.AuthUser {
-			password = T.config.AuthPassword
+		if user.Name == T.AuthUser {
+			password = T.AuthPassword
 		} else {
 			// dial admin connection
 			if admin == nil {
-				raw, err := net.Dial("tcp", primaryAddress)
-				if err != nil {
-					return discovery.Cluster{}, err
-				}
-				admin = fed.WrapNetConn(raw)
-				_, err = backends.Accept(&backends.AcceptContext{
-					Conn: admin,
-					Options: backends.AcceptOptions{
-						SSLMode: bouncer.SSLModePrefer,
-						SSLConfig: &tls.Config{
-							InsecureSkipVerify: true,
-						},
-						Username:    T.config.AuthUser,
-						Credentials: credentials.FromString(T.config.AuthUser, T.config.AuthPassword),
-						Database:    c.Databases[0],
+				admin, err = recipe.Dialer{
+					Network: "tcp",
+					Address: primaryAddress,
+					SSLMode: bouncer.SSLModePrefer,
+					SSLConfig: &tls.Config{
+						InsecureSkipVerify: true,
 					},
-				})
+					Username:    T.AuthUser,
+					Credentials: credentials.FromString(T.AuthUser, T.AuthPassword),
+					Database:    c.Databases[0],
+				}.Dial()
 				if err != nil {
 					return discovery.Cluster{}, err
 				}
@@ -141,7 +148,7 @@ func (T *Discoverer) instanceToCluster(primary *sqladmin.DatabaseInstance, repli
 			if err != nil {
 				return discovery.Cluster{}, err
 			}
-			_, err, err2 := bouncers.Bounce(client, admin, initialPacket)
+			_, err, err2 := bouncers.Bounce(fed.NewConn(client), admin, initialPacket)
 			if err != nil {
 				return discovery.Cluster{}, err
 			}
@@ -162,7 +169,7 @@ func (T *Discoverer) instanceToCluster(primary *sqladmin.DatabaseInstance, repli
 }
 
 func (T *Discoverer) Clusters() ([]discovery.Cluster, error) {
-	clusters, err := T.google.Instances.List(T.config.Project).Do()
+	clusters, err := T.google.Instances.List(T.Project).Do()
 	if err != nil {
 		return nil, err
 	}
@@ -201,12 +208,9 @@ func (T *Discoverer) Added() <-chan discovery.Cluster {
 	return nil
 }
 
-func (T *Discoverer) Updated() <-chan discovery.Cluster {
-	return nil
-}
-
 func (T *Discoverer) Removed() <-chan string {
 	return nil
 }
 
 var _ discovery.Discoverer = (*Discoverer)(nil)
+var _ caddy.Module = (*Discoverer)(nil)
diff --git a/lib/gat/handlers/discovery/discoverers/zalando_operator/config.go b/lib/gat/handlers/discovery/discoverers/zalando_operator/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..fab4714a95597959ad66cdb00e77eb93f87d72e0
--- /dev/null
+++ b/lib/gat/handlers/discovery/discoverers/zalando_operator/config.go
@@ -0,0 +1,7 @@
+package zalando_operator
+
+type Config struct {
+	Namespace                   string `json:"namespace"`
+	ConfigMapName               string `json:"config_map_name"`
+	OperatorConfigurationObject string `json:"operator_configuration_object"`
+}
diff --git a/lib/gat/modules/zalando_operator_discovery/discoverer.go b/lib/gat/handlers/discovery/discoverers/zalando_operator/discoverer.go
similarity index 69%
rename from lib/gat/modules/zalando_operator_discovery/discoverer.go
rename to lib/gat/handlers/discovery/discoverers/zalando_operator/discoverer.go
index 3e2adf38d51f46f9318b542f0036c1e738fe932d..62f547c46a6cdb20ff6cf5995499188b96c84b3f 100644
--- a/lib/gat/modules/zalando_operator_discovery/discoverer.go
+++ b/lib/gat/handlers/discovery/discoverers/zalando_operator/discoverer.go
@@ -1,11 +1,11 @@
-package zalando_operator_discovery
+package zalando_operator
 
 import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
+	"github.com/caddyserver/caddy/v2"
 	acidzalando "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do"
 	acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
 	acidv1informer "github.com/zalando/postgres-operator/pkg/generated/informers/externalversions/acid.zalan.do/v1"
@@ -14,13 +14,20 @@ import (
 	"github.com/zalando/postgres-operator/pkg/util/constants"
 	"github.com/zalando/postgres-operator/pkg/util/k8sutil"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
 
-	"gfx.cafe/gfx/pggat/lib/gat/modules/discovery"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/discovery"
 )
 
+func init() {
+	caddy.RegisterModule((*Discoverer)(nil))
+}
+
 type Discoverer struct {
-	config Config
+	Config
+
+	rest *rest.Config
 
 	op *config.Config
 
@@ -29,36 +36,41 @@ type Discoverer struct {
 	k8s k8sutil.KubernetesClient
 
 	added   chan discovery.Cluster
-	updated chan discovery.Cluster
 	removed chan string
+
+	done chan struct{}
 }
 
-func NewDiscoverer(conf Config) (*Discoverer, error) {
-	d := &Discoverer{
-		config: conf,
-	}
-	if err := d.init(); err != nil {
-		return nil, err
+func (T *Discoverer) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.discovery.discoverers.zalando_operator",
+		New: func() caddy.Module {
+			return new(Discoverer)
+		},
 	}
-	return d, nil
 }
 
-func (T *Discoverer) init() error {
+func (T *Discoverer) Provision(ctx caddy.Context) error {
 	var err error
-	T.k8s, err = k8sutil.NewFromConfig(T.config.Rest)
+	T.rest, err = rest.InClusterConfig()
 	if err != nil {
 		return err
 	}
 
-	if T.config.ConfigMapName != "" {
-		operatorConfig, err := T.k8s.ConfigMaps(T.config.Namespace).Get(context.Background(), T.config.ConfigMapName, metav1.GetOptions{})
+	T.k8s, err = k8sutil.NewFromConfig(T.rest)
+	if err != nil {
+		return err
+	}
+
+	if T.ConfigMapName != "" {
+		operatorConfig, err := T.k8s.ConfigMaps(T.Namespace).Get(ctx, T.ConfigMapName, metav1.GetOptions{})
 		if err != nil {
 			return nil
 		}
 
 		T.op = config.NewFromMap(operatorConfig.Data)
-	} else if T.config.OperatorConfigurationObject != "" {
-		operatorConfig, err := T.k8s.OperatorConfigurations(T.config.Namespace).Get(context.Background(), T.config.OperatorConfigurationObject, metav1.GetOptions{})
+	} else if T.OperatorConfigurationObject != "" {
+		operatorConfig, err := T.k8s.OperatorConfigurations(T.Namespace).Get(ctx, T.OperatorConfigurationObject, metav1.GetOptions{})
 		if err != nil {
 			return err
 		}
@@ -88,13 +100,12 @@ func (T *Discoverer) init() error {
 
 	T.informer = acidv1informer.NewPostgresqlInformer(
 		T.k8s.AcidV1ClientSet,
-		T.config.Namespace,
-		5*time.Minute,
+		T.Namespace,
+		0,
 		cache.Indexers{},
 	)
 
 	T.added = make(chan discovery.Cluster)
-	T.updated = make(chan discovery.Cluster)
 	T.removed = make(chan string)
 
 	_, err = T.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -118,7 +129,7 @@ func (T *Discoverer) init() error {
 			if err != nil {
 				return
 			}
-			T.updated <- cluster
+			T.added <- cluster
 		},
 		DeleteFunc: func(obj interface{}) {
 			psql, ok := obj.(*acidv1.Postgresql)
@@ -132,17 +143,23 @@ func (T *Discoverer) init() error {
 		return err
 	}
 
-	go T.informer.Run(nil)
+	T.done = make(chan struct{})
+	go T.informer.Run(T.done)
 
 	return nil
 }
 
+func (T *Discoverer) Cleanup() error {
+	close(T.done)
+	return nil
+}
+
 func (T *Discoverer) postgresqlToCluster(cluster acidv1.Postgresql) (discovery.Cluster, error) {
 	c := discovery.Cluster{
 		ID: string(cluster.UID),
 		Primary: discovery.Endpoint{
 			Network: "tcp",
-			Address: fmt.Sprintf("%s.%s.svc.%s:5432", cluster.Name, T.config.Namespace, T.op.ClusterDomain),
+			Address: fmt.Sprintf("%s.%s.svc.%s:5432", cluster.Name, T.Namespace, T.op.ClusterDomain),
 		},
 		Databases: make([]string, 0, len(cluster.Spec.Databases)),
 		Users:     make([]discovery.User, 0, len(cluster.Spec.Users)),
@@ -151,7 +168,7 @@ func (T *Discoverer) postgresqlToCluster(cluster acidv1.Postgresql) (discovery.C
 		c.Replicas = make(map[string]discovery.Endpoint, 1)
 		c.Replicas["repl"] = discovery.Endpoint{
 			Network: "tcp",
-			Address: fmt.Sprintf("%s-repl.%s.svc.%s:5432", cluster.Name, T.config.Namespace, T.op.ClusterDomain),
+			Address: fmt.Sprintf("%s-repl.%s.svc.%s:5432", cluster.Name, T.Namespace, T.op.ClusterDomain),
 		}
 	}
 
@@ -164,7 +181,7 @@ func (T *Discoverer) postgresqlToCluster(cluster acidv1.Postgresql) (discovery.C
 		)
 
 		// get secret
-		secret, err := T.k8s.Secrets(T.config.Namespace).Get(context.Background(), secretName, metav1.GetOptions{})
+		secret, err := T.k8s.Secrets(T.Namespace).Get(context.Background(), secretName, metav1.GetOptions{})
 		if err != nil {
 			return discovery.Cluster{}, err
 		}
@@ -188,19 +205,32 @@ func (T *Discoverer) postgresqlToCluster(cluster acidv1.Postgresql) (discovery.C
 }
 
 func (T *Discoverer) Clusters() ([]discovery.Cluster, error) {
-	return nil, nil
+	clusters, err := T.k8s.Postgresqls(T.Namespace).List(context.Background(), metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	res := make([]discovery.Cluster, 0, len(clusters.Items))
+	for _, cluster := range clusters.Items {
+		r, err := T.postgresqlToCluster(cluster)
+		if err != nil {
+			return nil, err
+		}
+		res = append(res, r)
+	}
+
+	return res, nil
 }
 
 func (T *Discoverer) Added() <-chan discovery.Cluster {
 	return T.added
 }
 
-func (T *Discoverer) Updated() <-chan discovery.Cluster {
-	return T.updated
-}
-
 func (T *Discoverer) Removed() <-chan string {
 	return T.removed
 }
 
 var _ discovery.Discoverer = (*Discoverer)(nil)
+var _ caddy.Module = (*Discoverer)(nil)
+var _ caddy.Provisioner = (*Discoverer)(nil)
+var _ caddy.CleanerUpper = (*Discoverer)(nil)
diff --git a/lib/gat/modules/discovery/module.go b/lib/gat/handlers/discovery/module.go
similarity index 54%
rename from lib/gat/modules/discovery/module.go
rename to lib/gat/handlers/discovery/module.go
index 8962fa8896fa9bd629411d1ae5e0cc7252cece95..bd6614d2e3f9350789e84dd0cb2637f060d85bbd 100644
--- a/lib/gat/modules/discovery/module.go
+++ b/lib/gat/handlers/discovery/module.go
@@ -1,42 +1,119 @@
 package discovery
 
 import (
+	"crypto/tls"
+	"fmt"
 	"sync"
 	"time"
 
-	"tuxpa.in/a/zlog/log"
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
 
 	"gfx.cafe/gfx/pggat/lib/auth"
 	"gfx.cafe/gfx/pggat/lib/auth/credentials"
+	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
+	"gfx.cafe/gfx/pggat/lib/fed"
 	"gfx.cafe/gfx/pggat/lib/gat"
 	"gfx.cafe/gfx/pggat/lib/gat/metrics"
 	"gfx.cafe/gfx/pggat/lib/gat/pool"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/pools/session"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/pools/transaction"
 	"gfx.cafe/gfx/pggat/lib/gat/pool/recipe"
 	"gfx.cafe/gfx/pggat/lib/util/maps"
 	"gfx.cafe/gfx/pggat/lib/util/slices"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
 type Module struct {
-	config Config
+	Config
+
+	discoverer Discoverer
+
+	pooler    gat.Pooler
+	sslConfig *tls.Config
+
+	serverStartupParameters map[strutil.CIString]string
+
+	closed chan struct{}
 
 	// this is fine to have no locking because it is only accessed by discoverLoop
 	clusters map[string]Cluster
 
-	pools maps.TwoKey[string, string, *pool.Pool]
+	pools maps.TwoKey[string, string, pool.WithCredentials]
 	mu    sync.RWMutex
+
+	log *zap.Logger
+}
+
+func (*Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.discovery",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
 }
 
-func NewModule(config Config) (*Module, error) {
-	m := &Module{
-		config: config,
+func (T *Module) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+
+	if T.Discoverer != nil {
+		val, err := ctx.LoadModule(T, "Discoverer")
+		if err != nil {
+			return fmt.Errorf("loading discoverer module: %v", err)
+		}
+		T.discoverer = val.(Discoverer)
+	}
+	if T.Pooler != nil {
+		val, err := ctx.LoadModule(T, "Pooler")
+		if err != nil {
+			return fmt.Errorf("loading pooler module: %v", err)
+		}
+		T.pooler = val.(gat.Pooler)
+	}
+	if T.ServerSSL != nil {
+		val, err := ctx.LoadModule(T, "ServerSSL")
+		if err != nil {
+			return fmt.Errorf("loading ssl module: %v", err)
+		}
+		ssl := val.(gat.SSLClient)
+		T.sslConfig = ssl.ClientTLSConfig()
+	}
+	T.serverStartupParameters = make(map[strutil.CIString]string, len(T.ServerStartupParameters))
+	for key, value := range T.ServerStartupParameters {
+		T.serverStartupParameters[strutil.MakeCIString(key)] = value
 	}
-	if err := m.reconcile(); err != nil {
-		return nil, err
+
+	if T.closed != nil {
+		return nil
 	}
-	go m.discoverLoop()
-	return m, nil
+	T.closed = make(chan struct{})
+
+	if err := T.reconcile(); err != nil {
+		return err
+	}
+	go T.discoverLoop()
+
+	return nil
+}
+
+func (T *Module) Cleanup() error {
+	if T.closed == nil {
+		return nil
+	}
+	close(T.closed)
+	T.closed = nil
+
+	T.mu.Lock()
+	defer T.mu.Unlock()
+	T.pools.Range(func(user string, database string, p pool.WithCredentials) bool {
+		p.Close()
+		T.pools.Delete(user, database)
+		return true
+	})
+	return nil
 }
 
 func (T *Module) replicaUsername(username string) string {
@@ -49,40 +126,11 @@ func (T *Module) creds(user User) (primary, replica auth.Credentials) {
 	return
 }
 
-func (T *Module) backendAcceptOptions(username string, creds auth.Credentials, database string) recipe.BackendAcceptOptions {
-	return recipe.BackendAcceptOptions{
-		SSLMode:           T.config.ServerSSLMode,
-		SSLConfig:         T.config.ServerSSLConfig,
-		Username:          username,
-		Credentials:       creds,
-		Database:          database,
-		StartupParameters: T.config.ServerStartupParameters,
-	}
-}
-
-func (T *Module) poolOptions(creds auth.Credentials) pool.Options {
-	options := pool.Options{
-		Credentials:                creds,
-		ServerReconnectInitialTime: T.config.ServerReconnectInitialTime,
-		ServerReconnectMaxTime:     T.config.ServerReconnectMaxTime,
-		ServerIdleTimeout:          T.config.ServerIdleTimeout,
-		TrackedParameters:          T.config.TrackedParameters,
-		ServerResetQuery:           T.config.ServerResetQuery,
-	}
-
-	switch T.config.PoolMode {
-	case "session":
-		options = session.Apply(options)
-	case "transaction":
-		options = transaction.Apply(options)
-	default:
-		log.Printf("unknown pool mode: %s", T.config.PoolMode)
-	}
-
-	return options
-}
-
 func (T *Module) added(cluster Cluster) {
+	if prev, ok := T.clusters[cluster.ID]; ok {
+		T.updated(prev, cluster)
+		return
+	}
 	if T.clusters == nil {
 		T.clusters = make(map[string]Cluster)
 	}
@@ -175,21 +223,24 @@ func (T *Module) replacePrimary(users []User, databases []string, endpoint Endpo
 	for _, user := range users {
 		primaryCreds, _ := T.creds(user)
 		for _, database := range databases {
-			acceptOptions := T.backendAcceptOptions(user.Username, primaryCreds, database)
-
 			primary := recipe.Dialer{
-				Network:       endpoint.Network,
-				Address:       endpoint.Address,
-				AcceptOptions: acceptOptions,
+				Network:           endpoint.Network,
+				Address:           endpoint.Address,
+				Username:          user.Username,
+				Credentials:       primaryCreds,
+				Database:          database,
+				SSLMode:           T.ServerSSLMode,
+				SSLConfig:         T.sslConfig,
+				StartupParameters: T.serverStartupParameters,
 			}
 
-			p := T.Lookup(user.Username, database)
-			if p == nil {
+			p, ok := T.lookup(user.Username, database)
+			if !ok {
 				continue
 			}
 
 			p.RemoveRecipe("primary")
-			p.AddRecipe("primary", recipe.NewRecipe(recipe.Options{
+			p.AddRecipe("primary", recipe.NewRecipe(recipe.Config{
 				Dialer: primary,
 			}))
 		}
@@ -201,18 +252,23 @@ func (T *Module) addReplicas(replicas map[string]Endpoint, users []User, databas
 		replicaUsername := T.replicaUsername(user.Username)
 		primaryCreds, replicaCreds := T.creds(user)
 		for _, database := range databases {
-			acceptOptions := T.backendAcceptOptions(user.Username, primaryCreds, database)
-
-			replicaPoolOptions := T.poolOptions(replicaCreds)
-			replicaPool := pool.NewPool(replicaPoolOptions)
+			replicaPool := pool.WithCredentials{
+				Pool:        T.pooler.NewPool(),
+				Credentials: replicaCreds,
+			}
 
 			for id, r := range replicas {
 				replica := recipe.Dialer{
-					Network:       r.Network,
-					Address:       r.Address,
-					AcceptOptions: acceptOptions,
+					Network:           r.Network,
+					Address:           r.Address,
+					Username:          user.Username,
+					Credentials:       primaryCreds,
+					Database:          database,
+					SSLMode:           T.ServerSSLMode,
+					SSLConfig:         T.sslConfig,
+					StartupParameters: T.serverStartupParameters,
 				}
-				replicaPool.AddRecipe(id, recipe.NewRecipe(recipe.Options{
+				replicaPool.AddRecipe(id, recipe.NewRecipe(recipe.Config{
 					Dialer: replica,
 				}))
 			}
@@ -236,19 +292,22 @@ func (T *Module) addReplica(users []User, databases []string, id string, endpoin
 		replicaUsername := T.replicaUsername(user.Username)
 		primaryCreds, _ := T.creds(user)
 		for _, database := range databases {
-			acceptOptions := T.backendAcceptOptions(user.Username, primaryCreds, database)
-
-			p := T.Lookup(replicaUsername, database)
-			if p == nil {
+			p, ok := T.lookup(replicaUsername, database)
+			if !ok {
 				continue
 			}
 
 			replica := recipe.Dialer{
-				Network:       endpoint.Network,
-				Address:       endpoint.Address,
-				AcceptOptions: acceptOptions,
+				Network:           endpoint.Network,
+				Address:           endpoint.Address,
+				Username:          user.Username,
+				Credentials:       primaryCreds,
+				Database:          database,
+				SSLMode:           T.ServerSSLMode,
+				SSLConfig:         T.sslConfig,
+				StartupParameters: T.serverStartupParameters,
 			}
-			p.AddRecipe(id, recipe.NewRecipe(recipe.Options{
+			p.AddRecipe(id, recipe.NewRecipe(recipe.Config{
 				Dialer: replica,
 			}))
 		}
@@ -259,8 +318,8 @@ func (T *Module) removeReplica(users []User, databases []string, id string) {
 	for _, user := range users {
 		username := T.replicaUsername(user.Username)
 		for _, database := range databases {
-			p := T.Lookup(username, database)
-			if p == nil {
+			p, ok := T.lookup(username, database)
+			if !ok {
 				continue
 			}
 			p.RemoveRecipe(id)
@@ -272,34 +331,39 @@ func (T *Module) addUser(primaryEndpoint Endpoint, replicas map[string]Endpoint,
 	replicaUsername := T.replicaUsername(user.Username)
 	primaryCreds, replicaCreds := T.creds(user)
 	for _, database := range databases {
-		acceptOptions := T.backendAcceptOptions(user.Username, primaryCreds, database)
-
-		primary := recipe.Dialer{
-			Network:       primaryEndpoint.Network,
-			Address:       primaryEndpoint.Address,
-			AcceptOptions: acceptOptions,
+		base := recipe.Dialer{
+			Username:          user.Username,
+			Credentials:       primaryCreds,
+			Database:          database,
+			SSLMode:           T.ServerSSLMode,
+			SSLConfig:         T.sslConfig,
+			StartupParameters: T.serverStartupParameters,
 		}
 
-		primaryPoolOptions := T.poolOptions(primaryCreds)
+		primary := base
+		primary.Network = primaryEndpoint.Network
+		primary.Address = primaryEndpoint.Address
 
-		primaryPool := pool.NewPool(primaryPoolOptions)
-		primaryPool.AddRecipe("primary", recipe.NewRecipe(recipe.Options{
+		primaryPool := pool.WithCredentials{
+			Pool:        T.pooler.NewPool(),
+			Credentials: primaryCreds,
+		}
+		primaryPool.AddRecipe("primary", recipe.NewRecipe(recipe.Config{
 			Dialer: primary,
 		}))
 		T.addPool(user.Username, database, primaryPool)
 
 		if len(replicas) > 0 {
-			replicaPoolOptions := T.poolOptions(replicaCreds)
-
-			replicaPool := pool.NewPool(replicaPoolOptions)
+			replicaPool := pool.WithCredentials{
+				Pool:        T.pooler.NewPool(),
+				Credentials: replicaCreds,
+			}
 
 			for id, r := range replicas {
-				replica := recipe.Dialer{
-					Network:       r.Network,
-					Address:       r.Address,
-					AcceptOptions: acceptOptions,
-				}
-				replicaPool.AddRecipe(id, recipe.NewRecipe(recipe.Options{
+				replica := base
+				replica.Network = r.Network
+				replica.Address = r.Address
+				replicaPool.AddRecipe(id, recipe.NewRecipe(recipe.Config{
 					Dialer: replica,
 				}))
 			}
@@ -326,34 +390,39 @@ func (T *Module) addDatabase(primaryEndpoint Endpoint, replicas map[string]Endpo
 		replicaUsername := T.replicaUsername(user.Username)
 		primaryCreds, replicaCreds := T.creds(user)
 
-		acceptOptions := T.backendAcceptOptions(user.Username, primaryCreds, database)
-
-		primary := recipe.Dialer{
-			Network:       primaryEndpoint.Network,
-			Address:       primaryEndpoint.Address,
-			AcceptOptions: acceptOptions,
+		base := recipe.Dialer{
+			Username:          user.Username,
+			Credentials:       primaryCreds,
+			Database:          database,
+			SSLMode:           T.ServerSSLMode,
+			SSLConfig:         T.sslConfig,
+			StartupParameters: T.serverStartupParameters,
 		}
 
-		primaryPoolOptions := T.poolOptions(primaryCreds)
+		primary := base
+		primary.Network = primaryEndpoint.Network
+		primary.Address = primaryEndpoint.Address
 
-		primaryPool := pool.NewPool(primaryPoolOptions)
-		primaryPool.AddRecipe("primary", recipe.NewRecipe(recipe.Options{
+		primaryPool := pool.WithCredentials{
+			Pool:        T.pooler.NewPool(),
+			Credentials: primaryCreds,
+		}
+		primaryPool.AddRecipe("primary", recipe.NewRecipe(recipe.Config{
 			Dialer: primary,
 		}))
 		T.addPool(user.Username, database, primaryPool)
 
 		if len(replicas) > 0 {
-			replicaPoolOptions := T.poolOptions(replicaCreds)
-
-			replicaPool := pool.NewPool(replicaPoolOptions)
+			replicaPool := pool.WithCredentials{
+				Pool:        T.pooler.NewPool(),
+				Credentials: replicaCreds,
+			}
 
 			for id, r := range replicas {
-				replica := recipe.Dialer{
-					Network:       r.Network,
-					Address:       r.Address,
-					AcceptOptions: acceptOptions,
-				}
-				replicaPool.AddRecipe(id, recipe.NewRecipe(recipe.Options{
+				replica := base
+				replica.Network = r.Network
+				replica.Address = r.Address
+				replicaPool.AddRecipe(id, recipe.NewRecipe(recipe.Config{
 					Dialer: replica,
 				}))
 			}
@@ -385,7 +454,7 @@ func (T *Module) removed(id string) {
 }
 
 func (T *Module) reconcile() error {
-	clusters, err := T.config.Discoverer.Clusters()
+	clusters, err := T.discoverer.Clusters()
 	if err != nil {
 		return err
 	}
@@ -415,33 +484,31 @@ outer:
 
 func (T *Module) discoverLoop() {
 	var reconcile <-chan time.Time
-	if T.config.ReconcilePeriod != 0 {
-		r := time.NewTicker(T.config.ReconcilePeriod)
+	if T.ReconcilePeriod != 0 {
+		r := time.NewTicker(T.ReconcilePeriod.Duration())
 		defer r.Stop()
 
 		reconcile = r.C
 	}
 	for {
 		select {
-		case cluster := <-T.config.Discoverer.Added():
+		case cluster := <-T.discoverer.Added():
 			T.added(cluster)
-		case id := <-T.config.Discoverer.Removed():
+		case id := <-T.discoverer.Removed():
 			T.removed(id)
-		case next := <-T.config.Discoverer.Updated():
-			T.updated(T.clusters[next.ID], next)
 		case <-reconcile:
 			err := T.reconcile()
 			if err != nil {
-				log.Printf("failed to reconcile: %v", err)
+				T.log.Warn("failed to reconcile", zap.Error(err))
 			}
 		}
 	}
 }
 
-func (T *Module) addPool(user, database string, p *pool.Pool) {
+func (T *Module) addPool(user, database string, p pool.WithCredentials) {
 	T.mu.Lock()
 	defer T.mu.Unlock()
-	log.Printf("added pool user=%s database=%s", user, database)
+	T.log.Info("added pool", zap.String("user", user), zap.String("database", database))
 	if old, ok := T.pools.Load(user, database); ok {
 		// shouldn't normally get here
 		old.Close()
@@ -457,27 +524,50 @@ func (T *Module) removePool(user, database string) {
 		return
 	}
 	p.Close()
-	log.Printf("removed pool user=%s database=%s", user, database)
+	T.log.Info("removed pool", zap.String("user", user), zap.String("database", database))
 	T.pools.Delete(user, database)
 }
 
-func (T *Module) GatModule() {}
-
-func (T *Module) ReadMetrics(metrics *metrics.Pools) {
+func (T *Module) ReadMetrics(metrics *metrics.Handler) {
 	T.mu.RLock()
 	defer T.mu.RUnlock()
-	T.pools.Range(func(_ string, _ string, p *pool.Pool) bool {
+	T.pools.Range(func(_ string, _ string, p pool.WithCredentials) bool {
 		p.ReadMetrics(&metrics.Pool)
 		return true
 	})
 }
 
-func (T *Module) Lookup(user, database string) *gat.Pool {
+func (T *Module) lookup(user, database string) (pool.WithCredentials, bool) {
 	T.mu.RLock()
 	defer T.mu.RUnlock()
-	p, _ := T.pools.Load(user, database)
-	return p
+	return T.pools.Load(user, database)
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	p, ok := T.lookup(conn.User, conn.Database)
+	if !ok {
+		return nil
+	}
+
+	if err := frontends.Authenticate(conn, p.Credentials); err != nil {
+		return err
+	}
+
+	return p.Serve(conn)
+}
+
+func (T *Module) Cancel(key [8]byte) {
+	T.mu.RLock()
+	defer T.mu.RUnlock()
+	T.pools.Range(func(_ string, _ string, p pool.WithCredentials) bool {
+		p.Cancel(key)
+		return true
+	})
 }
 
-var _ gat.Module = (*Module)(nil)
-var _ gat.Provider = (*Module)(nil)
+var _ gat.Handler = (*Module)(nil)
+var _ gat.MetricsHandler = (*Module)(nil)
+var _ gat.CancellableHandler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
+var _ caddy.Provisioner = (*Module)(nil)
+var _ caddy.CleanerUpper = (*Module)(nil)
diff --git a/lib/gat/handlers/error/module.go b/lib/gat/handlers/error/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..b8eedbf704d40f63e2308636c063aa3a68ba0a7f
--- /dev/null
+++ b/lib/gat/handlers/error/module.go
@@ -0,0 +1,36 @@
+package error_handler
+
+import (
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/perror"
+	"github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Message string `json:"message"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.error",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(_ *fed.Conn) error {
+	return perror.New(
+		perror.FATAL,
+		perror.InternalError,
+		T.Message,
+	)
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/modules/pgbouncer/authfile.go b/lib/gat/handlers/pgbouncer/authfile.go
similarity index 100%
rename from lib/gat/modules/pgbouncer/authfile.go
rename to lib/gat/handlers/pgbouncer/authfile.go
diff --git a/lib/gat/modules/pgbouncer/config.go b/lib/gat/handlers/pgbouncer/config.go
similarity index 89%
rename from lib/gat/modules/pgbouncer/config.go
rename to lib/gat/handlers/pgbouncer/config.go
index 1ce4b8edbeb6d07b7ba87273878d57714d514f04..6f3b179adbe9adf4901e9b04d18dc68f4f5cffcf 100644
--- a/lib/gat/modules/pgbouncer/config.go
+++ b/lib/gat/handlers/pgbouncer/config.go
@@ -1,7 +1,16 @@
 package pgbouncer
 
 import (
+	"encoding/json"
+	"net"
+	"strconv"
+	"strings"
+
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+
 	"gfx.cafe/gfx/pggat/lib/bouncer"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/ssl/servers/x509_key_pair"
 	"gfx.cafe/gfx/pggat/lib/util/encoding/ini"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
@@ -239,3 +248,49 @@ func Load(config string) (Config, error) {
 	err = ini.Unmarshal(conf, &c)
 	return c, err
 }
+
+func (T Config) Listen() []gat.ListenerConfig {
+	var ssl json.RawMessage
+	if T.PgBouncer.ClientTLSCertFile != "" && T.PgBouncer.ClientTLSKeyFile != "" {
+		ssl = caddyconfig.JSONModuleObject(
+			x509_key_pair.Server{
+				CertFile: T.PgBouncer.ClientTLSCertFile,
+				KeyFile:  T.PgBouncer.ClientTLSKeyFile,
+			},
+			"provider",
+			"x509_key_pair",
+			nil,
+		)
+	}
+
+	var listeners []gat.ListenerConfig
+
+	if T.PgBouncer.ListenAddr != "" {
+		listenAddr := T.PgBouncer.ListenAddr
+		if listenAddr == "*" {
+			listenAddr = ""
+		}
+
+		listen := net.JoinHostPort(listenAddr, strconv.Itoa(T.PgBouncer.ListenPort))
+
+		listeners = append(listeners, gat.ListenerConfig{
+			Address: listen,
+		})
+	}
+
+	// listen on unix socket
+	dir := T.PgBouncer.UnixSocketDir
+	port := T.PgBouncer.ListenPort
+
+	if !strings.HasSuffix(dir, "/") {
+		dir = dir + "/"
+	}
+	dir = dir + ".s.PGSQL." + strconv.Itoa(port)
+
+	listeners = append(listeners, gat.ListenerConfig{
+		Address: dir,
+		SSL:     ssl,
+	})
+
+	return listeners
+}
diff --git a/lib/gat/handlers/pgbouncer/module.go b/lib/gat/handlers/pgbouncer/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..a5a8f025fcdc70e1c491edd94bd21d854f08dc38
--- /dev/null
+++ b/lib/gat/handlers/pgbouncer/module.go
@@ -0,0 +1,363 @@
+package pgbouncer
+
+import (
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
+
+	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/session"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/transaction"
+	"gfx.cafe/gfx/pggat/lib/perror"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
+	"gfx.cafe/gfx/pggat/lib/util/slices"
+
+	"gfx.cafe/gfx/pggat/lib/auth/credentials"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/metrics"
+	"gfx.cafe/gfx/pggat/lib/gat/pool"
+	"gfx.cafe/gfx/pggat/lib/gat/pool/recipe"
+	"gfx.cafe/gfx/pggat/lib/gsql"
+	"gfx.cafe/gfx/pggat/lib/util/maps"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+type authQueryResult struct {
+	Username string `sql:"0"`
+	Password string `sql:"1"`
+}
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	ConfigFile string `json:"config"`
+	Config     Config `json:"-"`
+
+	pools maps.TwoKey[string, string, pool.WithCredentials]
+	mu    sync.RWMutex
+
+	log *zap.Logger
+}
+
+func (*Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.pgbouncer",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+
+	if T.ConfigFile != "" {
+		var err error
+		T.Config, err = Load(T.ConfigFile)
+		return err
+	}
+	return nil
+}
+
+func (T *Module) Cleanup() error {
+	T.mu.Lock()
+	defer T.mu.Unlock()
+
+	T.pools.Range(func(user string, database string, p pool.WithCredentials) bool {
+		p.Close()
+		T.pools.Delete(user, database)
+		return true
+	})
+
+	return nil
+}
+
+func (T *Module) getPassword(user, database string) (string, bool) {
+	// try to get password
+	password, ok := T.Config.PgBouncer.AuthFile[user]
+	if !ok {
+		// try to run auth query
+		if T.Config.PgBouncer.AuthQuery == "" {
+			return "", false
+		}
+
+		authUser := T.Config.Databases[database].AuthUser
+		if authUser == "" {
+			authUser = T.Config.PgBouncer.AuthUser
+			if authUser == "" {
+				return "", false
+			}
+		}
+
+		authPool, ok := T.lookup(authUser, database)
+		if !ok {
+			return "", false
+		}
+
+		var result authQueryResult
+		client := new(gsql.Client)
+		err := gsql.ExtendedQuery(client, &result, T.Config.PgBouncer.AuthQuery, user)
+		if err != nil {
+			T.log.Warn("auth query failed", zap.Error(err))
+			return "", false
+		}
+		err = client.Close()
+		if err != nil {
+			T.log.Warn("auth query failed", zap.Error(err))
+			return "", false
+		}
+		err = authPool.ServeBot(client)
+		if err != nil && !errors.Is(err, io.EOF) {
+			T.log.Warn("auth query failed", zap.Error(err))
+			return "", false
+		}
+
+		if result.Username != user {
+			// user not found
+			return "", false
+		}
+
+		password = result.Password
+	}
+
+	return password, true
+}
+
+func (T *Module) tryCreate(user, database string) (pool.WithCredentials, bool) {
+	db, ok := T.Config.Databases[database]
+	if !ok {
+		// try wildcard
+		db, ok = T.Config.Databases["*"]
+		if !ok {
+			return pool.WithCredentials{}, false
+		}
+	}
+
+	// try to get password
+	password, ok := T.getPassword(user, database)
+	if !ok {
+		return pool.WithCredentials{}, false
+	}
+
+	creds := credentials.FromString(user, password)
+
+	serverDatabase := db.DBName
+	if serverDatabase == "" {
+		serverDatabase = database
+	}
+
+	configUser := T.Config.Users[user]
+
+	poolMode := db.PoolMode
+	if poolMode == "" {
+		poolMode = configUser.PoolMode
+		if poolMode == "" {
+			poolMode = T.Config.PgBouncer.PoolMode
+		}
+	}
+
+	trackedParameters := append([]strutil.CIString{
+		strutil.MakeCIString("client_encoding"),
+		strutil.MakeCIString("datestyle"),
+		strutil.MakeCIString("timezone"),
+		strutil.MakeCIString("standard_conforming_strings"),
+		strutil.MakeCIString("application_name"),
+	}, T.Config.PgBouncer.TrackExtraParameters...)
+
+	serverLoginRetry := dur.Duration(T.Config.PgBouncer.ServerLoginRetry * float64(time.Second))
+
+	poolOptions := pool.Config{
+		ManagementConfig: pool.ManagementConfig{
+			TrackedParameters:          trackedParameters,
+			ServerResetQuery:           T.Config.PgBouncer.ServerResetQuery,
+			ServerIdleTimeout:          dur.Duration(T.Config.PgBouncer.ServerIdleTimeout * float64(time.Second)),
+			ServerReconnectInitialTime: serverLoginRetry,
+		},
+		Logger: T.log,
+	}
+
+	switch poolMode {
+	case PoolModeSession:
+		poolOptions.PoolingConfig = session.PoolingOptions
+	case PoolModeTransaction:
+		if T.Config.PgBouncer.ServerResetQueryAlways == 0 {
+			poolOptions.ServerResetQuery = ""
+		}
+		poolOptions.PoolingConfig = transaction.PoolingOptions
+	default:
+		return pool.WithCredentials{}, false
+	}
+	p := pool.WithCredentials{
+		Pool:        pool.NewPool(poolOptions),
+		Credentials: creds,
+	}
+
+	T.mu.Lock()
+	defer T.mu.Unlock()
+	T.pools.Store(user, database, p)
+
+	serverCreds := creds
+	if db.Password != "" {
+		// lookup password
+		serverCreds = credentials.FromString(user, db.Password)
+	}
+
+	dialer := recipe.Dialer{
+		SSLMode: T.Config.PgBouncer.ServerTLSSSLMode,
+		SSLConfig: &tls.Config{
+			InsecureSkipVerify: true, // TODO(garet)
+		},
+		Username:          user,
+		Credentials:       serverCreds,
+		Database:          serverDatabase,
+		StartupParameters: db.StartupParameters,
+	}
+
+	if db.Host == "" || strings.HasPrefix(db.Host, "/") {
+		// connect over unix socket
+		dir := db.Host
+		port := db.Port
+		if !strings.HasPrefix(dir, "/") {
+			dir = dir + "/"
+		}
+
+		if port == 0 {
+			port = 5432
+		}
+
+		dir = dir + ".s.PGSQL." + strconv.Itoa(port)
+
+		dialer.Network = "unix"
+		dialer.Address = dir
+	} else {
+		var address string
+		if db.Port == 0 {
+			address = net.JoinHostPort(db.Host, "5432")
+		} else {
+			address = net.JoinHostPort(db.Host, strconv.Itoa(db.Port))
+		}
+
+		// connect over tcp
+		dialer.Network = "tcp"
+		dialer.Address = address
+	}
+
+	recipeOptions := recipe.Config{
+		Dialer:         dialer,
+		MinConnections: db.MinPoolSize,
+		MaxConnections: db.MaxDBConnections,
+	}
+	if recipeOptions.MinConnections == 0 {
+		recipeOptions.MinConnections = T.Config.PgBouncer.MinPoolSize
+	}
+	if recipeOptions.MaxConnections == 0 {
+		recipeOptions.MaxConnections = T.Config.PgBouncer.MaxDBConnections
+	}
+	r := recipe.NewRecipe(recipeOptions)
+
+	p.AddRecipe("pgbouncer", r)
+
+	return p, true
+}
+
+func (T *Module) lookup(user, database string) (pool.WithCredentials, bool) {
+	p, ok := T.pools.Load(user, database)
+	if ok {
+		return p, true
+	}
+
+	// try to create pool
+	return T.tryCreate(user, database)
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	// check ssl
+	if T.Config.PgBouncer.ClientTLSSSLMode.IsRequired() {
+		var ssl bool
+		netConn, ok := conn.ReadWriteCloser.(*fed.NetConn)
+		if ok {
+			ssl = netConn.SSL()
+		}
+
+		if !ssl {
+			return perror.New(
+				perror.FATAL,
+				perror.InvalidPassword,
+				"SSL is required",
+			)
+		}
+	}
+
+	// check startup parameters
+	for key := range conn.InitialParameters {
+		if slices.Contains([]strutil.CIString{
+			strutil.MakeCIString("client_encoding"),
+			strutil.MakeCIString("datestyle"),
+			strutil.MakeCIString("timezone"),
+			strutil.MakeCIString("standard_conforming_strings"),
+			strutil.MakeCIString("application_name"),
+		}, key) {
+			continue
+		}
+		if slices.Contains(T.Config.PgBouncer.TrackExtraParameters, key) {
+			continue
+		}
+		if slices.Contains(T.Config.PgBouncer.IgnoreStartupParameters, key) {
+			continue
+		}
+
+		return perror.New(
+			perror.FATAL,
+			perror.FeatureNotSupported,
+			fmt.Sprintf(`Startup parameter "%s" is not supported`, key.String()),
+		)
+	}
+
+	p, ok := T.lookup(conn.User, conn.Database)
+	if !ok {
+		return nil
+	}
+
+	if err := frontends.Authenticate(conn, p.Credentials); err != nil {
+		return err
+	}
+
+	return p.Serve(conn)
+}
+
+func (T *Module) ReadMetrics(metrics *metrics.Handler) {
+	T.mu.RLock()
+	defer T.mu.RUnlock()
+	T.pools.Range(func(_ string, _ string, p pool.WithCredentials) bool {
+		p.ReadMetrics(&metrics.Pool)
+		return true
+	})
+}
+
+func (T *Module) Cancel(key [8]byte) {
+	T.mu.RLock()
+	defer T.mu.RUnlock()
+	T.pools.Range(func(_ string, _ string, p pool.WithCredentials) bool {
+		p.Cancel(key)
+		return true
+	})
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ gat.MetricsHandler = (*Module)(nil)
+var _ gat.CancellableHandler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
+var _ caddy.Provisioner = (*Module)(nil)
+var _ caddy.CleanerUpper = (*Module)(nil)
diff --git a/lib/gat/handlers/pgbouncer_spilo/config.go b/lib/gat/handlers/pgbouncer_spilo/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..f303c6eb99b7091d81e6ded8b1ddfabf6e399a53
--- /dev/null
+++ b/lib/gat/handlers/pgbouncer_spilo/config.go
@@ -0,0 +1,15 @@
+package pgbouncer_spilo
+
+type Config struct {
+	Host          string `json:"host"`
+	Port          int    `json:"port"`
+	User          string `json:"user"`
+	Schema        string `json:"schema"`
+	Password      string `json:"password"`
+	Mode          string `json:"mode"`
+	DefaultSize   int    `json:"default_size"`
+	MinSize       int    `json:"min_size"`
+	ReserveSize   int    `json:"reserve_size"`
+	MaxClientConn int    `json:"max_client_conn"`
+	MaxDBConn     int    `json:"max_db_conn"`
+}
diff --git a/lib/gat/modules/zalando/module.go b/lib/gat/handlers/pgbouncer_spilo/module.go
similarity index 53%
rename from lib/gat/modules/zalando/module.go
rename to lib/gat/handlers/pgbouncer_spilo/module.go
index 061e3ea25ec0e36b9afb3437c3dc4df3e53e7219..dc41720b5b91196e3dd19d2f6d636d5dc5412f23 100644
--- a/lib/gat/modules/zalando/module.go
+++ b/lib/gat/handlers/pgbouncer_spilo/module.go
@@ -1,32 +1,54 @@
-package zalando
+package pgbouncer_spilo
 
 import (
 	"fmt"
 
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer"
+
 	"gfx.cafe/gfx/pggat/lib/bouncer"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/pgbouncer"
+	"gfx.cafe/gfx/pggat/lib/gat"
+
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-func NewModule(config Config) (*pgbouncer.Module, error) {
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Config
+
+	pgbouncer.Module `json:"-"`
+}
+
+func (*Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.pgbouncer_spilo",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Provision(ctx caddy.Context) error {
 	pgb := pgbouncer.Default
 	if pgb.Databases == nil {
 		pgb.Databases = make(map[string]pgbouncer.Database)
 	}
 	pgb.Databases["*"] = pgbouncer.Database{
-		Host:     config.PGHost,
-		Port:     config.PGPort,
-		AuthUser: config.PGUser,
+		Host:     T.Host,
+		Port:     T.Port,
+		AuthUser: T.User,
 	}
-	pgb.PgBouncer.PoolMode = pgbouncer.PoolMode(config.PoolerMode)
-	pgb.PgBouncer.ListenPort = config.PoolerPort
-	pgb.PgBouncer.ListenAddr = "*"
+	pgb.PgBouncer.PoolMode = pgbouncer.PoolMode(T.Mode)
 	pgb.PgBouncer.AuthType = "md5"
 	pgb.PgBouncer.AuthFile = pgbouncer.AuthFile{
-		config.PGUser: config.PGPassword,
+		T.User: T.Password,
 	}
-	pgb.PgBouncer.AdminUsers = []string{config.PGUser}
-	pgb.PgBouncer.AuthQuery = fmt.Sprintf("SELECT * FROM %s.user_lookup($1)", config.PGSchema)
+	pgb.PgBouncer.AdminUsers = []string{T.User}
+	pgb.PgBouncer.AuthQuery = fmt.Sprintf("SELECT * FROM %s.user_lookup($1)", T.Schema)
 	pgb.PgBouncer.LogFile = "/var/log/pgbouncer/pgbouncer.log"
 	pgb.PgBouncer.PidFile = "/var/run/pgbouncer/pgbouncer.pid"
 
@@ -42,10 +64,10 @@ func NewModule(config Config) (*pgbouncer.Module, error) {
 	pgb.PgBouncer.LogConnections = 0
 	pgb.PgBouncer.LogDisconnections = 0
 
-	pgb.PgBouncer.DefaultPoolSize = config.PoolerDefaultSize
-	pgb.PgBouncer.ReservePoolSize = config.PoolerReserveSize
-	pgb.PgBouncer.MaxClientConn = config.PoolerMaxClientConn
-	pgb.PgBouncer.MaxDBConnections = config.PoolerMaxDBConn
+	pgb.PgBouncer.DefaultPoolSize = T.DefaultSize
+	pgb.PgBouncer.ReservePoolSize = T.ReserveSize
+	pgb.PgBouncer.MaxClientConn = T.MaxClientConn
+	pgb.PgBouncer.MaxDBConnections = T.MaxDBConn
 	pgb.PgBouncer.IdleTransactionTimeout = 600
 	pgb.PgBouncer.ServerLoginRetry = 5
 
@@ -54,5 +76,13 @@ func NewModule(config Config) (*pgbouncer.Module, error) {
 		strutil.MakeCIString("options"),
 	}
 
-	return pgbouncer.NewModule(pgb)
+	T.Module = pgbouncer.Module{
+		Config: pgb,
+	}
+
+	return nil
 }
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
+var _ caddy.Provisioner = (*Module)(nil)
diff --git a/lib/gat/handlers/pool/config.go b/lib/gat/handlers/pool/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..5a2fc336fd46a6aee54c26118aa067f7cdb855c4
--- /dev/null
+++ b/lib/gat/handlers/pool/config.go
@@ -0,0 +1,22 @@
+package pool_handler
+
+import (
+	"encoding/json"
+
+	"gfx.cafe/gfx/pggat/lib/bouncer"
+)
+
+type Config struct {
+	Pooler json.RawMessage `json:"pooler" caddy:"namespace=pggat.poolers inline_key=pooler"`
+
+	// Server connect options
+	ServerAddress string          `jsonn:"server_address"`
+	ServerSSLMode bouncer.SSLMode `json:"server_ssl_mode,omitempty"`
+	ServerSSL     json.RawMessage `json:"server_ssl,omitempty" caddy:"namespace=pggat.ssl.clients inline_key=provider"`
+
+	// Server routing options
+	ServerUsername          string            `json:"server_username"`
+	ServerPassword          string            `json:"server_password"`
+	ServerDatabase          string            `json:"server_database"`
+	ServerStartupParameters map[string]string `json:"server_startup_parameters"`
+}
diff --git a/lib/gat/handlers/pool/module.go b/lib/gat/handlers/pool/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..61b0085a7abd477aac62cd69d0d83d37df226374
--- /dev/null
+++ b/lib/gat/handlers/pool/module.go
@@ -0,0 +1,113 @@
+package pool_handler
+
+import (
+	"crypto/tls"
+	"fmt"
+	"strings"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/auth/credentials"
+	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/metrics"
+	"gfx.cafe/gfx/pggat/lib/gat/pool/recipe"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Config
+
+	pool *gat.Pool
+}
+
+func (*Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.pool",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Provision(ctx caddy.Context) error {
+	val, err := ctx.LoadModule(T, "Pooler")
+	if err != nil {
+		return fmt.Errorf("loading pooler module: %v", err)
+	}
+	pooler := val.(gat.Pooler)
+
+	var sslConfig *tls.Config
+	if T.ServerSSL != nil {
+		val, err = ctx.LoadModule(T, "ServerSSL")
+		if err != nil {
+			return fmt.Errorf("loading ssl module: %v", err)
+		}
+		ssl := val.(gat.SSLClient)
+		sslConfig = ssl.ClientTLSConfig()
+	}
+
+	creds := credentials.FromString(T.ServerUsername, T.ServerPassword)
+	startupParameters := make(map[strutil.CIString]string, len(T.ServerStartupParameters))
+	for key, value := range T.ServerStartupParameters {
+		startupParameters[strutil.MakeCIString(key)] = value
+	}
+
+	var network string
+	if strings.HasPrefix(T.ServerAddress, "/") {
+		network = "unix"
+	} else {
+		network = "tcp"
+	}
+
+	d := recipe.Dialer{
+		Network:           network,
+		Address:           T.ServerAddress,
+		SSLMode:           T.ServerSSLMode,
+		SSLConfig:         sslConfig,
+		Username:          T.ServerUsername,
+		Credentials:       creds,
+		Database:          T.ServerDatabase,
+		StartupParameters: startupParameters,
+	}
+
+	T.pool = pooler.NewPool()
+	T.pool.AddRecipe("pool", recipe.NewRecipe(recipe.Config{
+		Dialer: d,
+	}))
+
+	return nil
+}
+
+func (T *Module) Cleanup() error {
+	T.pool.Close()
+	return nil
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	if err := frontends.Authenticate(conn, nil); err != nil {
+		return err
+	}
+
+	return T.pool.Serve(conn)
+}
+
+func (T *Module) Cancel(key [8]byte) {
+	T.pool.Cancel(key)
+}
+
+func (T *Module) ReadMetrics(metrics *metrics.Handler) {
+	T.pool.ReadMetrics(&metrics.Pool)
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ gat.MetricsHandler = (*Module)(nil)
+var _ gat.CancellableHandler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
+var _ caddy.Provisioner = (*Module)(nil)
+var _ caddy.CleanerUpper = (*Module)(nil)
diff --git a/lib/gat/handlers/require_ssl/module.go b/lib/gat/handlers/require_ssl/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..47f7cb8e63bb3f6ce31fd810505154aa1817a941
--- /dev/null
+++ b/lib/gat/handlers/require_ssl/module.go
@@ -0,0 +1,58 @@
+package require_ssl
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/perror"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	SSL bool `json:"ssl"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.require_ssl",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	var ssl bool
+
+	sslConn, ok := conn.ReadWriteCloser.(fed.SSL)
+	if ok {
+		ssl = sslConn.SSL()
+	}
+
+	if T.SSL {
+		if !ssl {
+			return perror.New(
+				perror.FATAL,
+				perror.InvalidPassword,
+				"SSL is required",
+			)
+		}
+		return nil
+	}
+
+	if ssl {
+		return perror.New(
+			perror.FATAL,
+			perror.InvalidPassword,
+			"SSL is not allowed",
+		)
+	}
+	return nil
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/handlers/rewrite_database/module.go b/lib/gat/handlers/rewrite_database/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..bd471975c538e5c1512de4750cc897da1df4cdb0
--- /dev/null
+++ b/lib/gat/handlers/rewrite_database/module.go
@@ -0,0 +1,34 @@
+package rewrite_database
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Database string `json:"database"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.rewrite_database",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	conn.Database = T.Database
+
+	return nil
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/handlers/rewrite_parameter/module.go b/lib/gat/handlers/rewrite_parameter/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..89b056fb57c4ef8248bcbcfc454e17270dc207aa
--- /dev/null
+++ b/lib/gat/handlers/rewrite_parameter/module.go
@@ -0,0 +1,39 @@
+package rewrite_parameter
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Key   strutil.CIString `json:"key"`
+	Value string           `json:"value"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.rewrite_parameter",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	if conn.InitialParameters == nil {
+		conn.InitialParameters = make(map[strutil.CIString]string)
+	}
+	conn.InitialParameters[T.Key] = T.Value
+
+	return nil
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/handlers/rewrite_password/module.go b/lib/gat/handlers/rewrite_password/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..0cec7729092223be5d0599c5e1f8d5b2ee68bc47
--- /dev/null
+++ b/lib/gat/handlers/rewrite_password/module.go
@@ -0,0 +1,37 @@
+package rewrite_password
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/auth/credentials"
+	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	Password string `json:"password"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.rewrite_password",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	return frontends.Authenticate(
+		conn,
+		credentials.FromString(conn.User, T.Password),
+	)
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/handlers/rewrite_user/module.go b/lib/gat/handlers/rewrite_user/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..a4a301cd6b14f0b789caf81b27e0ee9401508363
--- /dev/null
+++ b/lib/gat/handlers/rewrite_user/module.go
@@ -0,0 +1,34 @@
+package rewrite_user
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	User string `json:"user"`
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.handlers.rewrite_user",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Handle(conn *fed.Conn) error {
+	conn.User = T.User
+
+	return nil
+}
+
+var _ gat.Handler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/listen.go b/lib/gat/listen.go
new file mode 100644
index 0000000000000000000000000000000000000000..baa1090cef04d4ea7c474b0e98d3807af28d2c0f
--- /dev/null
+++ b/lib/gat/listen.go
@@ -0,0 +1,101 @@
+package gat
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+	"strconv"
+	"strings"
+
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+)
+
+type ListenerConfig struct {
+	Address string          `json:"address"`
+	SSL     json.RawMessage `json:"ssl,omitempty" caddy:"namespace=pggat.ssl.servers inline_key=provider"`
+}
+
+type Listener struct {
+	ListenerConfig
+
+	networkAddress caddy.NetworkAddress
+	ssl            SSLServer
+
+	listener net.Listener
+
+	log *zap.Logger
+}
+
+func (T *Listener) accept() (*fed.Conn, error) {
+	raw, err := T.listener.Accept()
+	if err != nil {
+		return nil, err
+	}
+	return fed.NewConn(
+		fed.NewNetConn(raw),
+	), nil
+}
+
+func (T *Listener) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+
+	if strings.HasPrefix(T.Address, "/") {
+		// unix address
+		T.networkAddress = caddy.NetworkAddress{
+			Network: "unix",
+			Host:    T.Address,
+		}
+	} else {
+		// tcp address
+		host, rawPort, ok := strings.Cut(T.Address, ":")
+
+		var port = 5432
+		if ok {
+			var err error
+			port, err = strconv.Atoi(rawPort)
+			if err != nil {
+				return fmt.Errorf("parsing port: %v", err)
+			}
+		}
+
+		T.networkAddress = caddy.NetworkAddress{
+			Network:   "tcp",
+			Host:      host,
+			StartPort: uint(port),
+			EndPort:   uint(port),
+		}
+	}
+
+	if T.SSL != nil {
+		val, err := ctx.LoadModule(T, "SSL")
+		if err != nil {
+			return fmt.Errorf("loading ssl module: %v", err)
+		}
+		T.ssl = val.(SSLServer)
+	}
+
+	return nil
+}
+
+func (T *Listener) Start() error {
+	listener, err := T.networkAddress.Listen(context.Background(), 0, net.ListenConfig{})
+	if err != nil {
+		return err
+	}
+	T.listener = listener.(net.Listener)
+
+	T.log.Info("listening", zap.String("address", T.listener.Addr().String()))
+
+	return nil
+}
+
+func (T *Listener) Stop() error {
+	return T.listener.Close()
+}
+
+var _ caddy.App = (*Listener)(nil)
+var _ caddy.Provisioner = (*Listener)(nil)
diff --git a/lib/gat/listener.go b/lib/gat/listener.go
deleted file mode 100644
index 71893dbab6362b65b1fce73af27ccd8384fd7370..0000000000000000000000000000000000000000
--- a/lib/gat/listener.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package gat
-
-type Listener interface {
-	Module
-
-	Endpoints() []Endpoint
-}
diff --git a/lib/gat/matcher.go b/lib/gat/matcher.go
new file mode 100644
index 0000000000000000000000000000000000000000..6152f4e455f0b398a69340df1799f27fd3723682
--- /dev/null
+++ b/lib/gat/matcher.go
@@ -0,0 +1,7 @@
+package gat
+
+import "gfx.cafe/gfx/pggat/lib/fed"
+
+type Matcher interface {
+	Matches(conn *fed.Conn) bool
+}
diff --git a/lib/gat/matchers/and.go b/lib/gat/matchers/and.go
new file mode 100644
index 0000000000000000000000000000000000000000..e4106ed9846f357d87333f9688afd08735183e89
--- /dev/null
+++ b/lib/gat/matchers/and.go
@@ -0,0 +1,59 @@
+package matchers
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*And)(nil))
+}
+
+type And struct {
+	And []json.RawMessage `json:"and" caddy:"namespace=pggat.matchers inline_key=matcher"`
+
+	and []gat.Matcher
+}
+
+func (T *And) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.and",
+		New: func() caddy.Module {
+			return new(And)
+		},
+	}
+}
+
+func (T *And) Provision(ctx caddy.Context) error {
+	T.and = make([]gat.Matcher, 0, len(T.And))
+	if T.And != nil {
+		val, err := ctx.LoadModule(T, "And")
+		if err != nil {
+			return fmt.Errorf("loading matcher module: %v", err)
+		}
+
+		for _, vv := range val.([]any) {
+			T.and = append(T.and, vv.(gat.Matcher))
+		}
+	}
+
+	return nil
+}
+
+func (T *And) Matches(conn *fed.Conn) bool {
+	for _, matcher := range T.and {
+		if !matcher.Matches(conn) {
+			return false
+		}
+	}
+	return true
+}
+
+var _ gat.Matcher = (*And)(nil)
+var _ caddy.Module = (*And)(nil)
+var _ caddy.Provisioner = (*And)(nil)
diff --git a/lib/gat/matchers/database.go b/lib/gat/matchers/database.go
new file mode 100644
index 0000000000000000000000000000000000000000..d75731ac17771fb0caa23f6b0e918f5e5dae53b3
--- /dev/null
+++ b/lib/gat/matchers/database.go
@@ -0,0 +1,32 @@
+package matchers
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Database)(nil))
+}
+
+type Database struct {
+	Database string `json:"database"`
+}
+
+func (T *Database) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.database",
+		New: func() caddy.Module {
+			return new(Database)
+		},
+	}
+}
+
+func (T *Database) Matches(conn *fed.Conn) bool {
+	return conn.Database == T.Database
+}
+
+var _ gat.Matcher = (*Database)(nil)
+var _ caddy.Module = (*Database)(nil)
diff --git a/lib/gat/matchers/localaddress.go b/lib/gat/matchers/localaddress.go
new file mode 100644
index 0000000000000000000000000000000000000000..c9ac3093f475bf7f3b6ddd3a9c6f128ff68f3933
--- /dev/null
+++ b/lib/gat/matchers/localaddress.go
@@ -0,0 +1,87 @@
+package matchers
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*LocalAddress)(nil))
+}
+
+type LocalAddress struct {
+	Network string `json:"network"`
+	Address string `json:"address"`
+
+	addr net.Addr
+}
+
+func (T *LocalAddress) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.local_address",
+		New: func() caddy.Module {
+			return new(LocalAddress)
+		},
+	}
+}
+
+func (T *LocalAddress) Provision(ctx caddy.Context) error {
+	var err error
+	switch T.Network {
+	case "tcp", "tcp4", "tcp6":
+		T.addr, err = net.ResolveTCPAddr(T.Network, T.Address)
+	case "udp", "udp4", "udp6":
+		T.addr, err = net.ResolveUDPAddr(T.Network, T.Address)
+	case "ip", "ip4", "ip6":
+		T.addr, err = net.ResolveIPAddr(T.Network, T.Address)
+	case "unix", "unixgram", "unixpacket":
+		T.addr, err = net.ResolveUnixAddr(T.Network, T.Address)
+	default:
+		err = fmt.Errorf("unknown network: %s", T.Network)
+	}
+	return err
+}
+
+func (T *LocalAddress) Matches(conn *fed.Conn) bool {
+	netConn, ok := conn.ReadWriteCloser.(*fed.NetConn)
+	if !ok {
+		return false
+	}
+	switch addr := netConn.LocalAddr().(type) {
+	case *net.TCPAddr:
+		expected, ok := T.addr.(*net.TCPAddr)
+		if !ok {
+			return false
+		}
+		return addr.Port == expected.Port && addr.Zone == expected.Zone && (expected.IP == nil || addr.IP.Equal(expected.IP))
+	case *net.IPAddr:
+		expected, ok := T.addr.(*net.IPAddr)
+		if !ok {
+			return false
+		}
+		return addr.Zone == expected.Zone && (expected.IP == nil || addr.IP.Equal(expected.IP))
+	case *net.UDPAddr:
+		expected, ok := T.addr.(*net.UDPAddr)
+		if !ok {
+			return false
+		}
+		return addr.Port == expected.Port && addr.Zone == expected.Zone && (expected.IP == nil || addr.IP.Equal(expected.IP))
+	case *net.UnixAddr:
+		expected, ok := T.addr.(*net.UnixAddr)
+		if !ok {
+			return false
+		}
+		return addr.Name == expected.Name && addr.Net == expected.Net
+	default:
+		return false
+	}
+}
+
+var _ gat.Matcher = (*LocalAddress)(nil)
+var _ caddy.Module = (*LocalAddress)(nil)
+var _ caddy.Provisioner = (*LocalAddress)(nil)
diff --git a/lib/gat/matchers/not.go b/lib/gat/matchers/not.go
new file mode 100644
index 0000000000000000000000000000000000000000..22e7facd65365abc35524ef2f09b436fec1044e1
--- /dev/null
+++ b/lib/gat/matchers/not.go
@@ -0,0 +1,51 @@
+package matchers
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Not)(nil))
+}
+
+type Not struct {
+	Not json.RawMessage `json:"not" caddy:"namespace=pggat.matchers inline_key=matcher"`
+
+	not gat.Matcher
+}
+
+func (T *Not) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.not",
+		New: func() caddy.Module {
+			return new(Not)
+		},
+	}
+}
+
+func (T *Not) Provision(ctx caddy.Context) error {
+	if T.Not != nil {
+		val, err := ctx.LoadModule(T, "Not")
+		if err != nil {
+			return fmt.Errorf("loading matcher module: %v", err)
+		}
+
+		T.not = val.(gat.Matcher)
+	}
+
+	return nil
+}
+
+func (T *Not) Matches(conn *fed.Conn) bool {
+	return !T.not.Matches(conn)
+}
+
+var _ gat.Matcher = (*Not)(nil)
+var _ caddy.Module = (*Not)(nil)
+var _ caddy.Provisioner = (*Not)(nil)
diff --git a/lib/gat/matchers/or.go b/lib/gat/matchers/or.go
new file mode 100644
index 0000000000000000000000000000000000000000..baf7e89f75d4669a52c2c924eb99ccac76f08059
--- /dev/null
+++ b/lib/gat/matchers/or.go
@@ -0,0 +1,59 @@
+package matchers
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Or)(nil))
+}
+
+type Or struct {
+	Or []json.RawMessage `json:"or" caddy:"namespace=pggat.matchers inline_key=matcher"`
+
+	or []gat.Matcher
+}
+
+func (T *Or) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.or",
+		New: func() caddy.Module {
+			return new(Or)
+		},
+	}
+}
+
+func (T *Or) Provision(ctx caddy.Context) error {
+	T.or = make([]gat.Matcher, 0, len(T.Or))
+	if T.Or != nil {
+		val, err := ctx.LoadModule(T, "Or")
+		if err != nil {
+			return fmt.Errorf("loading matcher module: %v", err)
+		}
+
+		for _, vv := range val.([]any) {
+			T.or = append(T.or, vv.(gat.Matcher))
+		}
+	}
+
+	return nil
+}
+
+func (T *Or) Matches(conn *fed.Conn) bool {
+	for _, matcher := range T.or {
+		if matcher.Matches(conn) {
+			return true
+		}
+	}
+	return false
+}
+
+var _ gat.Matcher = (*Or)(nil)
+var _ caddy.Module = (*Or)(nil)
+var _ caddy.Provisioner = (*Or)(nil)
diff --git a/lib/gat/matchers/ssl.go b/lib/gat/matchers/ssl.go
new file mode 100644
index 0000000000000000000000000000000000000000..57ae1a0acb907587d5c30070051af0a5a30d4017
--- /dev/null
+++ b/lib/gat/matchers/ssl.go
@@ -0,0 +1,36 @@
+package matchers
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*SSL)(nil))
+}
+
+type SSL struct {
+	SSL bool `json:"ssl"`
+}
+
+func (T *SSL) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.ssl",
+		New: func() caddy.Module {
+			return new(SSL)
+		},
+	}
+}
+
+func (T *SSL) Matches(conn *fed.Conn) bool {
+	sslConn, ok := conn.ReadWriteCloser.(fed.SSL)
+	if !ok {
+		return T.SSL == false
+	}
+	return sslConn.SSL() == T.SSL
+}
+
+var _ gat.Matcher = (*SSL)(nil)
+var _ caddy.Module = (*SSL)(nil)
diff --git a/lib/gat/matchers/startupparameter.go b/lib/gat/matchers/startupparameter.go
new file mode 100644
index 0000000000000000000000000000000000000000..2cbacf2425e96c8f32ae5f53c8ba0418ba717471
--- /dev/null
+++ b/lib/gat/matchers/startupparameter.go
@@ -0,0 +1,34 @@
+package matchers
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
+)
+
+func init() {
+	caddy.RegisterModule((*StartupParameter)(nil))
+}
+
+type StartupParameter struct {
+	Key   strutil.CIString `json:"key"`
+	Value string           `json:"value"`
+}
+
+func (T *StartupParameter) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.startup_parameter",
+		New: func() caddy.Module {
+			return new(StartupParameter)
+		},
+	}
+}
+
+func (T *StartupParameter) Matches(conn *fed.Conn) bool {
+	return conn.InitialParameters[T.Key] == T.Value
+}
+
+var _ gat.Matcher = (*StartupParameter)(nil)
+var _ caddy.Module = (*StartupParameter)(nil)
diff --git a/lib/gat/matchers/user.go b/lib/gat/matchers/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..33f94cf09fd6cdfe626432452a9c4cf7bf231744
--- /dev/null
+++ b/lib/gat/matchers/user.go
@@ -0,0 +1,32 @@
+package matchers
+
+import (
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*User)(nil))
+}
+
+type User struct {
+	User string `json:"user"`
+}
+
+func (T *User) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.matchers.user",
+		New: func() caddy.Module {
+			return new(User)
+		},
+	}
+}
+
+func (T *User) Matches(conn *fed.Conn) bool {
+	return conn.User == T.User
+}
+
+var _ gat.Matcher = (*User)(nil)
+var _ caddy.Module = (*User)(nil)
diff --git a/lib/gat/metrics/pools.go b/lib/gat/metrics/pools.go
index 3c929867ebb26ee150ecd8c64d11e96d52632e3a..bfd4f05c8d713efe03e4b89830ecac21a511c6bb 100644
--- a/lib/gat/metrics/pools.go
+++ b/lib/gat/metrics/pools.go
@@ -1,6 +1,6 @@
 package metrics
 
-type Pools struct {
+type Handler struct {
 	// TODO(garet)
 	Pool
 }
diff --git a/lib/gat/metrics/server.go b/lib/gat/metrics/server.go
index 0c6ed7170b830d1cff7abe0c774787e8514adabc..5abb26d33a8d82dbb5e4443ddc3ce3e0c3b5b5a7 100644
--- a/lib/gat/metrics/server.go
+++ b/lib/gat/metrics/server.go
@@ -2,5 +2,5 @@ package metrics
 
 type Server struct {
 	// TODO(garet)
-	Pools
+	Handler
 }
diff --git a/lib/gat/module.go b/lib/gat/module.go
deleted file mode 100644
index 0ec77c1684af9d0f78fd46c73493fa1ebbf33306..0000000000000000000000000000000000000000
--- a/lib/gat/module.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package gat
-
-type Module interface {
-	GatModule()
-}
diff --git a/lib/gat/modules/cloud_sql_discovery/config.go b/lib/gat/modules/cloud_sql_discovery/config.go
deleted file mode 100644
index c6f84166304f6e0cc05941b9475bc4a52053575d..0000000000000000000000000000000000000000
--- a/lib/gat/modules/cloud_sql_discovery/config.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package cloud_sql_discovery
-
-import (
-	"errors"
-
-	"gfx.cafe/util/go/gun"
-)
-
-type Config struct {
-	Project       string `env:"PGGAT_GC_PROJECT"`
-	IpAddressType string `env:"PGGAT_GC_IP_ADDR_TYPE" default:"PRIMARY"`
-	AuthUser      string `env:"PGGAT_GC_AUTH_USER" default:"pggat"`
-	AuthPassword  string `env:"PGGAT_GC_AUTH_PASSWORD"`
-}
-
-func Load() (Config, error) {
-	var conf Config
-	gun.Load(&conf)
-	if conf.Project == "" {
-		return Config{}, errors.New("expected PGGAT_GC_PROJECT")
-	}
-	return conf, nil
-}
diff --git a/lib/gat/modules/cloud_sql_discovery/module.go b/lib/gat/modules/cloud_sql_discovery/module.go
deleted file mode 100644
index 3cee645f095d1a240925b7da024fa3767904a6b5..0000000000000000000000000000000000000000
--- a/lib/gat/modules/cloud_sql_discovery/module.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package cloud_sql_discovery
-
-import (
-	"crypto/tls"
-	"time"
-
-	"gfx.cafe/gfx/pggat/lib/bouncer"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/discovery"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
-)
-
-func NewModule(config Config) (*discovery.Module, error) {
-	d, err := NewDiscoverer(config)
-	if err != nil {
-		return nil, err
-	}
-
-	return discovery.NewModule(discovery.Config{
-		ReconcilePeriod: 5 * time.Minute,
-		Discoverer:      d,
-		ServerSSLMode:   bouncer.SSLModePrefer,
-		ServerSSLConfig: &tls.Config{
-			InsecureSkipVerify: true,
-		},
-		ServerReconnectInitialTime: 5 * time.Second,
-		ServerReconnectMaxTime:     5 * time.Second,
-		ServerIdleTimeout:          5 * time.Minute,
-		TrackedParameters: []strutil.CIString{
-			strutil.MakeCIString("client_encoding"),
-			strutil.MakeCIString("datestyle"),
-			strutil.MakeCIString("timezone"),
-			strutil.MakeCIString("standard_conforming_strings"),
-			strutil.MakeCIString("application_name"),
-		},
-		PoolMode: "transaction", // TODO(garet)
-	})
-}
diff --git a/lib/gat/modules/digitalocean_discovery/config.go b/lib/gat/modules/digitalocean_discovery/config.go
deleted file mode 100644
index d228a3fff2a08b53da3e2c6b9c7b6baa058a8e16..0000000000000000000000000000000000000000
--- a/lib/gat/modules/digitalocean_discovery/config.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package digitalocean_discovery
-
-import (
-	"errors"
-
-	"gfx.cafe/util/go/gun"
-)
-
-type Config struct {
-	APIKey   string `env:"PGGAT_DO_API_KEY"`
-	Private  bool   `env:"PGGAT_DO_PRIVATE"`
-	PoolMode string `env:"PGGAT_POOL_MODE"`
-}
-
-func Load() (Config, error) {
-	var conf Config
-	gun.Load(&conf)
-	if conf.APIKey == "" {
-		return Config{}, errors.New("expected auth token")
-	}
-
-	return conf, nil
-}
diff --git a/lib/gat/modules/digitalocean_discovery/module.go b/lib/gat/modules/digitalocean_discovery/module.go
deleted file mode 100644
index 24dc545c2337b33f84642704dba93514af4cedff..0000000000000000000000000000000000000000
--- a/lib/gat/modules/digitalocean_discovery/module.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package digitalocean_discovery
-
-import (
-	"crypto/tls"
-	"time"
-
-	"gfx.cafe/gfx/pggat/lib/bouncer"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/discovery"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
-)
-
-func NewModule(config Config) (*discovery.Module, error) {
-	d, err := NewDiscoverer(config)
-	if err != nil {
-		return nil, err
-	}
-
-	return discovery.NewModule(discovery.Config{
-		ReconcilePeriod: 5 * time.Minute,
-		Discoverer:      d,
-		ServerSSLMode:   bouncer.SSLModeRequire,
-		ServerSSLConfig: &tls.Config{
-			InsecureSkipVerify: true,
-		},
-		ServerReconnectInitialTime: 5 * time.Second,
-		ServerReconnectMaxTime:     5 * time.Second,
-		ServerIdleTimeout:          5 * time.Minute,
-		TrackedParameters: []strutil.CIString{
-			strutil.MakeCIString("client_encoding"),
-			strutil.MakeCIString("datestyle"),
-			strutil.MakeCIString("timezone"),
-			strutil.MakeCIString("standard_conforming_strings"),
-			strutil.MakeCIString("application_name"),
-		},
-		PoolMode: "transaction", // TODO(garet)
-	})
-}
diff --git a/lib/gat/modules/discovery/config.go b/lib/gat/modules/discovery/config.go
deleted file mode 100644
index b2633ddb98d84c088df8161bf9a61f6c811ca9a5..0000000000000000000000000000000000000000
--- a/lib/gat/modules/discovery/config.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package discovery
-
-import (
-	"crypto/tls"
-	"time"
-
-	"gfx.cafe/gfx/pggat/lib/bouncer"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
-)
-
-type Config struct {
-	// ReconcilePeriod is how often the module should check for changes. 0 = disable
-	ReconcilePeriod time.Duration
-
-	Discoverer Discoverer
-
-	ServerSSLMode              bouncer.SSLMode
-	ServerSSLConfig            *tls.Config
-	ServerStartupParameters    map[strutil.CIString]string
-	ServerReconnectInitialTime time.Duration
-	ServerReconnectMaxTime     time.Duration
-	ServerIdleTimeout          time.Duration
-	ServerResetQuery           string
-
-	TrackedParameters []strutil.CIString
-
-	PoolMode string
-}
diff --git a/lib/gat/modules/pgbouncer/module.go b/lib/gat/modules/pgbouncer/module.go
deleted file mode 100644
index 1acb9c836fc2a8bf801f773fc3165cf39572faf3..0000000000000000000000000000000000000000
--- a/lib/gat/modules/pgbouncer/module.go
+++ /dev/null
@@ -1,320 +0,0 @@
-package pgbouncer
-
-import (
-	"crypto/tls"
-	"errors"
-	"io"
-	"net"
-	"strconv"
-	"strings"
-	"time"
-
-	"tuxpa.in/a/zlog/log"
-
-	"gfx.cafe/gfx/pggat/lib/auth/credentials"
-	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
-	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
-	"gfx.cafe/gfx/pggat/lib/gat"
-	"gfx.cafe/gfx/pggat/lib/gat/metrics"
-	"gfx.cafe/gfx/pggat/lib/gat/pool"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/pools/session"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/pools/transaction"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/recipe"
-	"gfx.cafe/gfx/pggat/lib/gsql"
-	"gfx.cafe/gfx/pggat/lib/util/maps"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
-)
-
-type authQueryResult struct {
-	Username string `sql:"0"`
-	Password string `sql:"1"`
-}
-
-type Module struct {
-	config Config
-
-	pools maps.TwoKey[string, string, *gat.Pool]
-}
-
-func NewModule(config Config) (*Module, error) {
-	return &Module{
-		config: config,
-	}, nil
-}
-
-func (T *Module) getPassword(user, database string) (string, bool) {
-	// try to get password
-	password, ok := T.config.PgBouncer.AuthFile[user]
-	if !ok {
-		// try to run auth query
-		if T.config.PgBouncer.AuthQuery == "" {
-			return "", false
-		}
-
-		authUser := T.config.Databases[database].AuthUser
-		if authUser == "" {
-			authUser = T.config.PgBouncer.AuthUser
-			if authUser == "" {
-				return "", false
-			}
-		}
-
-		authPool := T.Lookup(authUser, database)
-		if authPool == nil {
-			return "", false
-		}
-
-		var result authQueryResult
-		client := new(gsql.Client)
-		err := gsql.ExtendedQuery(client, &result, T.config.PgBouncer.AuthQuery, user)
-		if err != nil {
-			log.Println("auth query failed:", err)
-			return "", false
-		}
-		err = client.Close()
-		if err != nil {
-			log.Println("auth query failed:", err)
-			return "", false
-		}
-		err = authPool.ServeBot(client)
-		if err != nil && !errors.Is(err, io.EOF) {
-			log.Println("auth query failed:", err)
-			return "", false
-		}
-
-		if result.Username != user {
-			// user not found
-			return "", false
-		}
-
-		password = result.Password
-	}
-
-	return password, true
-}
-
-func (T *Module) tryCreate(user, database string) *gat.Pool {
-	db, ok := T.config.Databases[database]
-	if !ok {
-		// try wildcard
-		db, ok = T.config.Databases["*"]
-		if !ok {
-			return nil
-		}
-	}
-
-	// try to get password
-	password, ok := T.getPassword(user, database)
-	if !ok {
-		return nil
-	}
-
-	creds := credentials.FromString(user, password)
-
-	serverDatabase := db.DBName
-	if serverDatabase == "" {
-		serverDatabase = database
-	}
-
-	configUser := T.config.Users[user]
-
-	poolMode := db.PoolMode
-	if poolMode == "" {
-		poolMode = configUser.PoolMode
-		if poolMode == "" {
-			poolMode = T.config.PgBouncer.PoolMode
-		}
-	}
-
-	trackedParameters := append([]strutil.CIString{
-		strutil.MakeCIString("client_encoding"),
-		strutil.MakeCIString("datestyle"),
-		strutil.MakeCIString("timezone"),
-		strutil.MakeCIString("standard_conforming_strings"),
-		strutil.MakeCIString("application_name"),
-	}, T.config.PgBouncer.TrackExtraParameters...)
-
-	serverLoginRetry := time.Duration(T.config.PgBouncer.ServerLoginRetry * float64(time.Second))
-
-	poolOptions := pool.Options{
-		Credentials:                creds,
-		TrackedParameters:          trackedParameters,
-		ServerResetQuery:           T.config.PgBouncer.ServerResetQuery,
-		ServerIdleTimeout:          time.Duration(T.config.PgBouncer.ServerIdleTimeout * float64(time.Second)),
-		ServerReconnectInitialTime: serverLoginRetry,
-	}
-
-	switch poolMode {
-	case PoolModeSession:
-		poolOptions = session.Apply(poolOptions)
-	case PoolModeTransaction:
-		if T.config.PgBouncer.ServerResetQueryAlways == 0 {
-			poolOptions.ServerResetQuery = ""
-		}
-		poolOptions = transaction.Apply(poolOptions)
-	default:
-		return nil
-	}
-	p := pool.NewPool(poolOptions)
-
-	T.pools.Store(user, database, p)
-
-	var d recipe.Dialer
-
-	serverCreds := creds
-	if db.Password != "" {
-		// lookup password
-		serverCreds = credentials.FromString(user, db.Password)
-	}
-
-	acceptOptions := backends.AcceptOptions{
-		SSLMode: T.config.PgBouncer.ServerTLSSSLMode,
-		SSLConfig: &tls.Config{
-			InsecureSkipVerify: true, // TODO(garet)
-		},
-		Username:          user,
-		Credentials:       serverCreds,
-		Database:          serverDatabase,
-		StartupParameters: db.StartupParameters,
-	}
-
-	if db.Host == "" || strings.HasPrefix(db.Host, "/") {
-		// connect over unix socket
-		dir := db.Host
-		port := db.Port
-		if !strings.HasPrefix(dir, "/") {
-			dir = dir + "/"
-		}
-
-		if port == 0 {
-			port = 5432
-		}
-
-		dir = dir + ".s.PGSQL." + strconv.Itoa(port)
-
-		d = recipe.Dialer{
-			Network:       "unix",
-			Address:       dir,
-			AcceptOptions: acceptOptions,
-		}
-	} else {
-		var address string
-		if db.Port == 0 {
-			address = net.JoinHostPort(db.Host, "5432")
-		} else {
-			address = net.JoinHostPort(db.Host, strconv.Itoa(db.Port))
-		}
-
-		// connect over tcp
-		d = recipe.Dialer{
-			Network:       "tcp",
-			Address:       address,
-			AcceptOptions: acceptOptions,
-		}
-	}
-
-	recipeOptions := recipe.Options{
-		Dialer:         d,
-		MinConnections: db.MinPoolSize,
-		MaxConnections: db.MaxDBConnections,
-	}
-	if recipeOptions.MinConnections == 0 {
-		recipeOptions.MinConnections = T.config.PgBouncer.MinPoolSize
-	}
-	if recipeOptions.MaxConnections == 0 {
-		recipeOptions.MaxConnections = T.config.PgBouncer.MaxDBConnections
-	}
-	r := recipe.NewRecipe(recipeOptions)
-
-	p.AddRecipe("pgbouncer", r)
-
-	return p
-}
-
-func (T *Module) Lookup(user, database string) *gat.Pool {
-	p, _ := T.pools.Load(user, database)
-	if p != nil {
-		return p
-	}
-
-	// try to create pool
-	return T.tryCreate(user, database)
-}
-
-func (T *Module) ReadMetrics(metrics *metrics.Pools) {
-	T.pools.Range(func(_ string, _ string, p *gat.Pool) bool {
-		p.ReadMetrics(&metrics.Pool)
-		return true
-	})
-}
-
-func (T *Module) Endpoints() []gat.Endpoint {
-	trackedParameters := append([]strutil.CIString{
-		strutil.MakeCIString("client_encoding"),
-		strutil.MakeCIString("datestyle"),
-		strutil.MakeCIString("timezone"),
-		strutil.MakeCIString("standard_conforming_strings"),
-		strutil.MakeCIString("application_name"),
-	}, T.config.PgBouncer.TrackExtraParameters...)
-
-	allowedStartupParameters := append(trackedParameters, T.config.PgBouncer.IgnoreStartupParameters...)
-	var sslConfig *tls.Config
-	if T.config.PgBouncer.ClientTLSCertFile != "" && T.config.PgBouncer.ClientTLSKeyFile != "" {
-		certificate, err := tls.LoadX509KeyPair(T.config.PgBouncer.ClientTLSCertFile, T.config.PgBouncer.ClientTLSKeyFile)
-		if err != nil {
-			log.Printf("error loading X509 keypair: %v", err)
-		} else {
-			sslConfig = &tls.Config{
-				Certificates: []tls.Certificate{
-					certificate,
-				},
-			}
-		}
-	}
-
-	acceptOptions := frontends.AcceptOptions{
-		SSLRequired:           T.config.PgBouncer.ClientTLSSSLMode.IsRequired(),
-		SSLConfig:             sslConfig,
-		AllowedStartupOptions: allowedStartupParameters,
-	}
-
-	var endpoints []gat.Endpoint
-
-	if T.config.PgBouncer.ListenAddr != "" {
-		listenAddr := T.config.PgBouncer.ListenAddr
-		if listenAddr == "*" {
-			listenAddr = ""
-		}
-
-		listen := net.JoinHostPort(listenAddr, strconv.Itoa(T.config.PgBouncer.ListenPort))
-
-		endpoints = append(endpoints, gat.Endpoint{
-			Network:       "tcp",
-			Address:       listen,
-			AcceptOptions: acceptOptions,
-		})
-	}
-
-	// listen on unix socket
-	dir := T.config.PgBouncer.UnixSocketDir
-	port := T.config.PgBouncer.ListenPort
-
-	if !strings.HasSuffix(dir, "/") {
-		dir = dir + "/"
-	}
-	dir = dir + ".s.PGSQL." + strconv.Itoa(port)
-
-	endpoints = append(endpoints, gat.Endpoint{
-		Network:       "unix",
-		Address:       dir,
-		AcceptOptions: acceptOptions,
-	})
-
-	return endpoints
-}
-
-func (T *Module) GatModule() {}
-
-var _ gat.Module = (*Module)(nil)
-var _ gat.Provider = (*Module)(nil)
-var _ gat.Listener = (*Module)(nil)
diff --git a/lib/gat/modules/raw_pools/module.go b/lib/gat/modules/raw_pools/module.go
deleted file mode 100644
index 14ab7262f5316a602d0f5aa602491f324b1d8309..0000000000000000000000000000000000000000
--- a/lib/gat/modules/raw_pools/module.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package raw_pools
-
-import (
-	"sync"
-
-	"gfx.cafe/gfx/pggat/lib/gat"
-	"gfx.cafe/gfx/pggat/lib/gat/metrics"
-	"gfx.cafe/gfx/pggat/lib/gat/pool"
-	"gfx.cafe/gfx/pggat/lib/util/maps"
-)
-
-type Module struct {
-	pools maps.TwoKey[string, string, *pool.Pool]
-	mu    sync.RWMutex
-}
-
-func NewModule() (*Module, error) {
-	return &Module{}, nil
-}
-
-func (T *Module) GatModule() {}
-
-func (T *Module) Add(user, database string, p *pool.Pool) {
-	T.mu.Lock()
-	defer T.mu.Unlock()
-
-	T.pools.Store(user, database, p)
-}
-
-func (T *Module) Remove(user, database string) {
-	T.mu.Lock()
-	defer T.mu.Unlock()
-
-	T.pools.Delete(user, database)
-}
-
-func (T *Module) Lookup(user, database string) *gat.Pool {
-	T.mu.RLock()
-	defer T.mu.RUnlock()
-
-	p, _ := T.pools.Load(user, database)
-	return p
-}
-
-func (T *Module) ReadMetrics(metrics *metrics.Pools) {
-	T.mu.RLock()
-	defer T.mu.RUnlock()
-
-	T.pools.Range(func(_ string, _ string, p *pool.Pool) bool {
-		p.ReadMetrics(&metrics.Pool)
-		return true
-	})
-}
-
-var _ gat.Module = (*Module)(nil)
-var _ gat.Provider = (*Module)(nil)
diff --git a/lib/gat/modules/zalando/config.go b/lib/gat/modules/zalando/config.go
deleted file mode 100644
index ad174dee0e9ead966fda8caf83c5f211929fa3d3..0000000000000000000000000000000000000000
--- a/lib/gat/modules/zalando/config.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package zalando
-
-import (
-	"errors"
-
-	"gfx.cafe/util/go/gun"
-)
-
-type Config struct {
-	PGHost              string `env:"PGHOST"`
-	PGPort              int    `env:"PGPORT"`
-	PGUser              string `env:"PGUSER"`
-	PGSchema            string `env:"PGSCHEMA"`
-	PGPassword          string `env:"PGPASSWORD"`
-	PoolerPort          int    `env:"CONNECTION_POOLER_PORT"`
-	PoolerMode          string `env:"CONNECTION_POOLER_MODE"`
-	PoolerDefaultSize   int    `env:"CONNECTION_POOLER_DEFAULT_SIZE"`
-	PoolerMinSize       int    `env:"CONNECTION_POOLER_MIN_SIZE"`
-	PoolerReserveSize   int    `env:"CONNECTION_POOLER_RESERVE_SIZE"`
-	PoolerMaxClientConn int    `env:"CONNECTION_POOLER_MAX_CLIENT_CONN"`
-	PoolerMaxDBConn     int    `env:"CONNECTION_POOLER_MAX_DB_CONN"`
-}
-
-func Load() (Config, error) {
-	var conf Config
-	gun.Load(&conf)
-	if conf.PoolerMode == "" {
-		return Config{}, errors.New("expected pooler mode")
-	}
-
-	return conf, nil
-}
diff --git a/lib/gat/modules/zalando_operator_discovery/config.go b/lib/gat/modules/zalando_operator_discovery/config.go
deleted file mode 100644
index c4a4945147b5a87353830c0b42ca0d3adfaff80b..0000000000000000000000000000000000000000
--- a/lib/gat/modules/zalando_operator_discovery/config.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package zalando_operator_discovery
-
-import (
-	"gfx.cafe/util/go/gun"
-	"k8s.io/client-go/rest"
-)
-
-type Config struct {
-	Namespace                   string `env:"PGGAT_NAMESPACE" default:"default"`
-	ConfigMapName               string `env:"CONFIG_MAP_NAME"`
-	OperatorConfigurationObject string `env:"POSTGRES_OPERATOR_CONFIGURATION_OBJECT"`
-
-	Rest *rest.Config
-}
-
-func Load() (Config, error) {
-	var config Config
-	gun.Load(&config)
-
-	var err error
-	config.Rest, err = rest.InClusterConfig()
-	if err != nil {
-		return Config{}, err
-	}
-	return config, nil
-}
diff --git a/lib/gat/modules/zalando_operator_discovery/module.go b/lib/gat/modules/zalando_operator_discovery/module.go
deleted file mode 100644
index e3702a15ba0422a879135cbee8d7e115edc97f23..0000000000000000000000000000000000000000
--- a/lib/gat/modules/zalando_operator_discovery/module.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package zalando_operator_discovery
-
-import (
-	"crypto/tls"
-	"time"
-
-	"gfx.cafe/gfx/pggat/lib/bouncer"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/discovery"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
-)
-
-func NewModule(config Config) (*discovery.Module, error) {
-	d, err := NewDiscoverer(config)
-	if err != nil {
-		return nil, err
-	}
-	m, err := discovery.NewModule(discovery.Config{
-		Discoverer:    d,
-		ServerSSLMode: bouncer.SSLModePrefer,
-		ServerSSLConfig: &tls.Config{
-			InsecureSkipVerify: true,
-		},
-		ServerReconnectInitialTime: 5 * time.Second,
-		ServerReconnectMaxTime:     5 * time.Second,
-		ServerIdleTimeout:          5 * time.Minute,
-		// ServerResetQuery: "discard all",
-		TrackedParameters: []strutil.CIString{
-			strutil.MakeCIString("client_encoding"),
-			strutil.MakeCIString("datestyle"),
-			strutil.MakeCIString("timezone"),
-			strutil.MakeCIString("standard_conforming_strings"),
-			strutil.MakeCIString("application_name"),
-		},
-		PoolMode: "transaction", // TODO(garet) pool mode from operator config
-	})
-	if err != nil {
-		return nil, err
-	}
-	return m, nil
-}
diff --git a/lib/gat/pool/client.go b/lib/gat/pool/client.go
index 2d687e10229b09be35905ad882daf27ac27eee44..c6d3c76e45367c7843c3ad651a9bf694c538f3c8 100644
--- a/lib/gat/pool/client.go
+++ b/lib/gat/pool/client.go
@@ -2,12 +2,9 @@ package pool
 
 import (
 	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/middleware"
-	"gfx.cafe/gfx/pggat/lib/middleware/interceptor"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/eqp"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/ps"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/unterminate"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/eqp"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/ps"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/unterminate"
 )
 
 type pooledClient struct {
@@ -18,39 +15,31 @@ type pooledClient struct {
 }
 
 func newClient(
-	options Options,
-	conn fed.Conn,
-	initialParameters map[strutil.CIString]string,
-	backendKey [8]byte,
+	options Config,
+	conn *fed.Conn,
 ) *pooledClient {
-	middlewares := []middleware.Middleware{
+	conn.Middleware = append(
+		conn.Middleware,
 		unterminate.Unterminate,
-	}
+	)
 
 	var psClient *ps.Client
 	if options.ParameterStatusSync == ParameterStatusSyncDynamic {
 		// add ps middleware
-		psClient = ps.NewClient(initialParameters)
-		middlewares = append(middlewares, psClient)
+		psClient = ps.NewClient(conn.InitialParameters)
+		conn.Middleware = append(conn.Middleware, psClient)
 	}
 
 	var eqpClient *eqp.Client
 	if options.ExtendedQuerySync {
 		// add eqp middleware
 		eqpClient = eqp.NewClient()
-		middlewares = append(middlewares, eqpClient)
+		conn.Middleware = append(conn.Middleware, eqpClient)
 	}
 
-	conn = interceptor.NewInterceptor(
-		conn,
-		middlewares...,
-	)
-
 	return &pooledClient{
 		pooledConn: makeConn(
 			conn,
-			initialParameters,
-			backendKey,
 		),
 		ps:  psClient,
 		eqp: eqpClient,
diff --git a/lib/gat/pool/options.go b/lib/gat/pool/config.go
similarity index 73%
rename from lib/gat/pool/options.go
rename to lib/gat/pool/config.go
index a682419ba82aedc1fae8414bcc6933453609a891..36604526c2916b47a33c13b5e2dff5a9a81ae8d8 100644
--- a/lib/gat/pool/options.go
+++ b/lib/gat/pool/config.go
@@ -1,9 +1,9 @@
 package pool
 
 import (
-	"time"
+	"go.uber.org/zap"
 
-	"gfx.cafe/gfx/pggat/lib/auth"
+	"gfx.cafe/gfx/pggat/lib/util/dur"
 	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
@@ -20,33 +20,42 @@ const (
 	ParameterStatusSyncDynamic
 )
 
-type Options struct {
-	Credentials auth.Credentials
-
+type PoolingConfig struct {
 	NewPooler func() Pooler
 	// ReleaseAfterTransaction toggles whether servers should be released and re acquired after each transaction.
 	// Use false for lower latency
 	// Use true for better balancing
 	ReleaseAfterTransaction bool
 
-	ServerResetQuery string
+	// ParameterStatusSync is the parameter syncing mode
+	ParameterStatusSync ParameterStatusSync
+
+	// ExtendedQuerySync controls whether prepared statements and portals should be tracked and synced before use.
+	// Use false for lower latency
+	// Use true for transaction pooling
+	ExtendedQuerySync bool
+}
+
+type ManagementConfig struct {
+	ServerResetQuery string `json:"server_reset_query,omitempty"`
 	// ServerIdleTimeout defines how long a server may be idle before it is disconnected
-	ServerIdleTimeout time.Duration
+	ServerIdleTimeout dur.Duration `json:"server_idle_timeout,omitempty"`
 
 	// ServerReconnectInitialTime defines how long to wait initially before attempting a server reconnect
 	// 0 = disable, don't retry
-	ServerReconnectInitialTime time.Duration
+	ServerReconnectInitialTime dur.Duration `json:"server_reconnect_initial_time,omitempty"`
 	// ServerReconnectMaxTime defines the max amount of time to wait before attempting a server reconnect
 	// 0 = disable, back off infinitely
-	ServerReconnectMaxTime time.Duration
+	ServerReconnectMaxTime dur.Duration `json:"server_reconnect_max_time,omitempty"`
 
-	// ParameterStatusSync is the parameter syncing mode
-	ParameterStatusSync ParameterStatusSync
 	// TrackedParameters are parameters which should be synced by updating the server, not the client.
-	TrackedParameters []strutil.CIString
+	TrackedParameters []strutil.CIString `json:"tracked_parameters,omitempty"`
+}
 
-	// ExtendedQuerySync controls whether prepared statements and portals should be tracked and synced before use.
-	// Use false for lower latency
-	// Use true for transaction pooling
-	ExtendedQuerySync bool
+type Config struct {
+	PoolingConfig
+
+	ManagementConfig
+
+	Logger *zap.Logger
 }
diff --git a/lib/gat/pool/conn.go b/lib/gat/pool/conn.go
index 8e321311f40d1b32ef67b61e04ee4751be5c6004..065a645895951e6693e756a2054e03f45f89b031 100644
--- a/lib/gat/pool/conn.go
+++ b/lib/gat/pool/conn.go
@@ -15,12 +15,7 @@ import (
 type pooledConn struct {
 	id uuid.UUID
 
-	conn fed.Conn
-	// please someone fix runtime.convI2I
-	rw fed.ReadWriter
-
-	initialParameters map[strutil.CIString]string
-	backendKey        [8]byte
+	conn *fed.Conn
 
 	// metrics
 
@@ -38,16 +33,11 @@ type pooledConn struct {
 }
 
 func makeConn(
-	conn fed.Conn,
-	initialParameters map[strutil.CIString]string,
-	backendKey [8]byte,
+	conn *fed.Conn,
 ) pooledConn {
 	return pooledConn{
-		id:                uuid.New(),
-		conn:              conn,
-		rw:                conn,
-		initialParameters: initialParameters,
-		backendKey:        backendKey,
+		id:   uuid.New(),
+		conn: conn,
 
 		since: time.Now(),
 	}
@@ -57,21 +47,16 @@ func (T *pooledConn) GetID() uuid.UUID {
 	return T.id
 }
 
-func (T *pooledConn) GetConn() fed.Conn {
+func (T *pooledConn) GetConn() *fed.Conn {
 	return T.conn
 }
 
-// GetReadWriter is the exact same as GetConn but bypasses the runtime.convI2I
-func (T *pooledConn) GetReadWriter() fed.ReadWriter {
-	return T.rw
-}
-
 func (T *pooledConn) GetInitialParameters() map[strutil.CIString]string {
-	return T.initialParameters
+	return T.conn.InitialParameters
 }
 
 func (T *pooledConn) GetBackendKey() [8]byte {
-	return T.backendKey
+	return T.conn.BackendKey
 }
 
 func (T *pooledConn) TransactionComplete() {
diff --git a/lib/gat/pool/errors.go b/lib/gat/pool/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..b0a18cecdeae5d460c0c6df65417ac908aaf926b
--- /dev/null
+++ b/lib/gat/pool/errors.go
@@ -0,0 +1,5 @@
+package pool
+
+import "errors"
+
+var ErrClosed = errors.New("pool closed")
diff --git a/lib/gat/pool/flow.go b/lib/gat/pool/flow.go
index a186ea7ec9779863a089a1e6419d9e78cfb0bfb9..e12636210e444859639229cf308bb4e48c2b6cec 100644
--- a/lib/gat/pool/flow.go
+++ b/lib/gat/pool/flow.go
@@ -3,14 +3,14 @@ package pool
 import (
 	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
 	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/eqp"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/ps"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/gat/metrics"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/eqp"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/ps"
 	"gfx.cafe/gfx/pggat/lib/util/slices"
 )
 
-func pair(options Options, client *pooledClient, server *pooledServer) (clientErr, serverErr error) {
+func pair(options Config, client *pooledClient, server *pooledServer) (clientErr, serverErr error) {
 	defer func() {
 		client.SetState(metrics.ConnStateActive, server.GetID())
 		server.SetState(metrics.ConnStateActive, client.GetID())
@@ -23,7 +23,7 @@ func pair(options Options, client *pooledClient, server *pooledServer) (clientEr
 
 	switch options.ParameterStatusSync {
 	case ParameterStatusSyncDynamic:
-		clientErr, serverErr = ps.Sync(options.TrackedParameters, client.GetReadWriter(), client.GetPS(), server.GetReadWriter(), server.GetPS())
+		clientErr, serverErr = ps.Sync(options.TrackedParameters, client.GetConn(), client.GetPS(), server.GetConn(), server.GetPS())
 	case ParameterStatusSyncInitial:
 		clientErr, serverErr = syncInitialParameters(options, client, server)
 	}
@@ -33,13 +33,13 @@ func pair(options Options, client *pooledClient, server *pooledServer) (clientEr
 	}
 
 	if options.ExtendedQuerySync {
-		serverErr = eqp.Sync(client.GetEQP(), server.GetReadWriter(), server.GetEQP())
+		serverErr = eqp.Sync(client.GetEQP(), server.GetConn(), server.GetEQP())
 	}
 
 	return
 }
 
-func syncInitialParameters(options Options, client *pooledClient, server *pooledServer) (clientErr, serverErr error) {
+func syncInitialParameters(options Config, client *pooledClient, server *pooledServer) (clientErr, serverErr error) {
 	clientParams := client.GetInitialParameters()
 	serverParams := server.GetInitialParameters()
 
@@ -80,15 +80,10 @@ func syncInitialParameters(options Options, client *pooledClient, server *pooled
 			continue
 		}
 
-		ctx := backends.Context{
-			Packet: packet,
-			Server: server.GetReadWriter(),
-		}
-		serverErr = backends.SetParameter(&ctx, key, value)
+		serverErr, _, packet = backends.SetParameter(server.GetConn(), nil, packet, key, value)
 		if serverErr != nil {
 			return
 		}
-		packet = ctx.Packet
 	}
 
 	for key, value := range serverParams {
diff --git a/lib/gat/pool/pool.go b/lib/gat/pool/pool.go
index 3ad6b328f89e9307cf135e82a2d6dc48fad57795..8399e39b80bb5ba6e913de1415d8a4dbafc9b7c1 100644
--- a/lib/gat/pool/pool.go
+++ b/lib/gat/pool/pool.go
@@ -7,21 +7,19 @@ import (
 	"time"
 
 	"github.com/google/uuid"
-	"tuxpa.in/a/zlog/log"
+	"go.uber.org/zap"
 
-	"gfx.cafe/gfx/pggat/lib/auth"
 	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
 	"gfx.cafe/gfx/pggat/lib/bouncer/bouncers/v2"
 	"gfx.cafe/gfx/pggat/lib/fed"
 	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/gat/metrics"
 	"gfx.cafe/gfx/pggat/lib/util/slices"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
 type Pool struct {
-	options Options
-	pooler  Pooler
+	config Config
+	pooler Pooler
 
 	closed chan struct{}
 
@@ -37,18 +35,18 @@ type Pool struct {
 	mu               sync.RWMutex
 }
 
-func NewPool(options Options) *Pool {
-	if options.NewPooler == nil {
+func NewPool(config Config) *Pool {
+	if config.NewPooler == nil {
 		panic("expected new pooler func")
 	}
-	pooler := options.NewPooler()
+	pooler := config.NewPooler()
 	if pooler == nil {
 		panic("expected pooler")
 	}
 
 	p := &Pool{
-		options: options,
-		pooler:  pooler,
+		config: config,
+		pooler: pooler,
 
 		closed:  make(chan struct{}),
 		pending: make(chan struct{}, 1),
@@ -79,10 +77,6 @@ func (T *Pool) idlest() (server *pooledServer, at time.Time) {
 	return
 }
 
-func (T *Pool) GetCredentials() auth.Credentials {
-	return T.options.Credentials
-}
-
 func (T *Pool) AddRecipe(name string, r *Recipe) {
 	func() {
 		T.mu.Lock()
@@ -104,7 +98,7 @@ func (T *Pool) AddRecipe(name string, r *Recipe) {
 	count := r.AllocateInitial()
 	for i := 0; i < count; i++ {
 		if err := T.scaleUpL1(name, r); err != nil {
-			log.Printf("failed to dial server: %v", err)
+			T.config.Logger.Warn("failed to dial server", zap.Error(err))
 			for j := i; j < count; j++ {
 				r.Free()
 			}
@@ -153,7 +147,7 @@ func (T *Pool) scaleUpL0() (string, *Recipe) {
 }
 
 func (T *Pool) scaleUpL1(name string, r *Recipe) error {
-	conn, params, err := r.Dial()
+	conn, err := r.Dial()
 	if err != nil {
 		// failed to dial
 		r.Free()
@@ -170,11 +164,9 @@ func (T *Pool) scaleUpL1(name string, r *Recipe) error {
 		}
 
 		server := newServer(
-			T.options,
+			T.config,
 			name,
 			conn,
-			params.InitialParameters,
-			params.BackendKey,
 		)
 
 		if T.servers == nil {
@@ -209,7 +201,7 @@ func (T *Pool) scaleUp() bool {
 
 	err := T.scaleUpL1(name, r)
 	if err != nil {
-		log.Printf("failed to dial server: %v", err)
+		T.config.Logger.Warn("failed to dial server", zap.Error(err))
 		return false
 	}
 
@@ -231,9 +223,12 @@ func (T *Pool) removeServerL1(server *pooledServer) {
 		name := server.GetRecipe()
 		T.serversByRecipe[name] = slices.Delete(T.serversByRecipe[name], server)
 		// update order
-		T.recipeScaleOrder.Update(slices.Index(T.recipeScaleOrder, name), func(n string) int {
-			return len(T.serversByRecipe[n])
-		})
+		index := slices.Index(T.recipeScaleOrder, name)
+		if index != -1 {
+			T.recipeScaleOrder.Update(index, func(n string) int {
+				return len(T.serversByRecipe[n])
+			})
+		}
 	}
 }
 
@@ -250,6 +245,9 @@ func (T *Pool) acquireServer(client *pooledClient) *pooledServer {
 			}
 			serverID = T.pooler.Acquire(client.GetID(), SyncModeBlocking)
 			T.pendingCount.Add(-1)
+			if serverID == uuid.Nil {
+				return nil
+			}
 		}
 
 		T.mu.RLock()
@@ -264,13 +262,10 @@ func (T *Pool) acquireServer(client *pooledClient) *pooledServer {
 }
 
 func (T *Pool) releaseServer(server *pooledServer) {
-	if T.options.ServerResetQuery != "" {
+	if T.config.ServerResetQuery != "" {
 		server.SetState(metrics.ConnStateRunningResetQuery, uuid.Nil)
 
-		ctx := backends.Context{
-			Server: server.GetReadWriter(),
-		}
-		err := backends.QueryString(&ctx, T.options.ServerResetQuery)
+		err, _, _ := backends.QueryString(server.GetConn(), nil, nil, T.config.ServerResetQuery)
 		if err != nil {
 			T.removeServer(server)
 			return
@@ -283,19 +278,15 @@ func (T *Pool) releaseServer(server *pooledServer) {
 }
 
 func (T *Pool) Serve(
-	conn fed.Conn,
-	initialParameters map[strutil.CIString]string,
-	backendKey [8]byte,
+	conn *fed.Conn,
 ) error {
 	defer func() {
 		_ = conn.Close()
 	}()
 
 	client := newClient(
-		T.options,
+		T.config,
 		conn,
-		initialParameters,
-		backendKey,
 	)
 
 	return T.serve(client, false)
@@ -304,17 +295,17 @@ func (T *Pool) Serve(
 // ServeBot is for clients that don't need initial parameters, cancelling queries, and are ready now. Use Serve for
 // real clients
 func (T *Pool) ServeBot(
-	conn fed.Conn,
+	conn fed.ReadWriteCloser,
 ) error {
 	defer func() {
 		_ = conn.Close()
 	}()
 
 	client := newClient(
-		T.options,
-		conn,
-		nil,
-		[8]byte{},
+		T.config,
+		&fed.Conn{
+			ReadWriteCloser: conn,
+		},
 	)
 
 	return T.serve(client, true)
@@ -343,8 +334,11 @@ func (T *Pool) serve(client *pooledClient, initialized bool) error {
 
 	if !initialized {
 		server = T.acquireServer(client)
+		if server == nil {
+			return ErrClosed
+		}
 
-		err, serverErr = pair(T.options, client, server)
+		err, serverErr = pair(T.config, client, server)
 		if serverErr != nil {
 			return serverErr
 		}
@@ -361,7 +355,7 @@ func (T *Pool) serve(client *pooledClient, initialized bool) error {
 	}
 
 	for {
-		if server != nil && T.options.ReleaseAfterTransaction {
+		if server != nil && T.config.ReleaseAfterTransaction {
 			client.SetState(metrics.ConnStateIdle, uuid.Nil)
 			T.releaseServer(server)
 			server = nil
@@ -374,11 +368,14 @@ func (T *Pool) serve(client *pooledClient, initialized bool) error {
 
 		if server == nil {
 			server = T.acquireServer(client)
+			if server == nil {
+				return ErrClosed
+			}
 
-			err, serverErr = pair(T.options, client, server)
+			err, serverErr = pair(T.config, client, server)
 		}
 		if err == nil && serverErr == nil {
-			packet, err, serverErr = bouncers.Bounce(client.GetReadWriter(), server.GetReadWriter(), packet)
+			packet, err, serverErr = bouncers.Bounce(client.GetConn(), server.GetConn(), packet)
 		}
 		if serverErr != nil {
 			return serverErr
@@ -421,23 +418,23 @@ func (T *Pool) removeClientL1(client *pooledClient) {
 	delete(T.clientsByKey, client.GetBackendKey())
 }
 
-func (T *Pool) Cancel(key [8]byte) error {
+func (T *Pool) Cancel(key [8]byte) {
 	T.mu.RLock()
 	defer T.mu.RUnlock()
 
 	client, ok := T.clientsByKey[key]
 	if !ok {
-		return nil
+		return
 	}
 
 	state, peer, _ := client.GetState()
 	if state != metrics.ConnStateActive {
-		return nil
+		return
 	}
 
 	server, ok := T.servers[peer]
 	if !ok {
-		return nil
+		return
 	}
 
 	// prevent state from changing by RLocking the server
@@ -446,15 +443,15 @@ func (T *Pool) Cancel(key [8]byte) error {
 
 	// make sure peer is still set
 	if server.peer != peer {
-		return nil
+		return
 	}
 
 	r, ok := T.recipes[server.recipe]
 	if !ok {
-		return nil
+		return
 	}
 
-	return r.Cancel(server.backendKey)
+	r.Cancel(server.GetBackendKey())
 }
 
 func (T *Pool) ReadMetrics(m *metrics.Pool) {
@@ -483,6 +480,7 @@ func (T *Pool) ReadMetrics(m *metrics.Pool) {
 
 func (T *Pool) Close() {
 	close(T.closed)
+	T.pooler.Close()
 
 	T.mu.Lock()
 	defer T.mu.Unlock()
diff --git a/lib/gat/pool/pooler.go b/lib/gat/pool/pooler.go
index 2133ce11ba5f39a43d55c00d0910067c574bed9d..06bba5a46ba99f4e34c4c69e8d196f02617799f8 100644
--- a/lib/gat/pool/pooler.go
+++ b/lib/gat/pool/pooler.go
@@ -18,4 +18,6 @@ type Pooler interface {
 
 	Acquire(client uuid.UUID, sync SyncMode) (server uuid.UUID)
 	Release(server uuid.UUID)
+
+	Close()
 }
diff --git a/lib/gat/pool/pools/session/apply.go b/lib/gat/pool/pools/session/apply.go
deleted file mode 100644
index 2c9f8157626ee3d098ddf1635878a2eed1ffc7ee..0000000000000000000000000000000000000000
--- a/lib/gat/pool/pools/session/apply.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package session
-
-import (
-	"gfx.cafe/gfx/pggat/lib/gat/pool"
-)
-
-func Apply(options pool.Options) pool.Options {
-	options.NewPooler = NewPooler
-	options.ParameterStatusSync = pool.ParameterStatusSyncInitial
-	options.ExtendedQuerySync = false
-	return options
-}
diff --git a/lib/gat/pool/pools/transaction/apply.go b/lib/gat/pool/pools/transaction/apply.go
deleted file mode 100644
index 088a11b35ec19d05b9bcc2336c3417ceef9ef419..0000000000000000000000000000000000000000
--- a/lib/gat/pool/pools/transaction/apply.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package transaction
-
-import "gfx.cafe/gfx/pggat/lib/gat/pool"
-
-func Apply(options pool.Options) pool.Options {
-	options.NewPooler = NewPooler
-	options.ParameterStatusSync = pool.ParameterStatusSyncDynamic
-	options.ExtendedQuerySync = true
-	options.ReleaseAfterTransaction = true
-	return options
-}
diff --git a/lib/gat/pool/recipe/options.go b/lib/gat/pool/recipe/config.go
similarity index 88%
rename from lib/gat/pool/recipe/options.go
rename to lib/gat/pool/recipe/config.go
index 6edc926c8f5e65fc58e3f36061bc81a97c05fb81..37ae8d6602e4cde1eed617841d36d3909cf1caf3 100644
--- a/lib/gat/pool/recipe/options.go
+++ b/lib/gat/pool/recipe/config.go
@@ -1,6 +1,6 @@
 package recipe
 
-type Options struct {
+type Config struct {
 	Dialer Dialer
 
 	MinConnections int
diff --git a/lib/gat/pool/recipe/dialer.go b/lib/gat/pool/recipe/dialer.go
index 622cc32898ce8c514b838c55e95c0485779dc786..8202c2b9a6b1633fb835e7c092d9e8a84757ee69 100644
--- a/lib/gat/pool/recipe/dialer.go
+++ b/lib/gat/pool/recipe/dialer.go
@@ -1,57 +1,68 @@
 package recipe
 
 import (
-	"errors"
-	"io"
+	"crypto/tls"
 	"net"
 
+	"gfx.cafe/gfx/pggat/lib/auth"
+	"gfx.cafe/gfx/pggat/lib/bouncer"
 	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
 	"gfx.cafe/gfx/pggat/lib/fed"
+	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-type BackendAcceptOptions = backends.AcceptOptions
-
 type Dialer struct {
 	Network string
 	Address string
 
-	AcceptOptions BackendAcceptOptions
+	SSLMode           bouncer.SSLMode
+	SSLConfig         *tls.Config
+	Username          string
+	Credentials       auth.Credentials
+	Database          string
+	StartupParameters map[strutil.CIString]string
 }
 
-func (T Dialer) Dial() (fed.Conn, backends.AcceptParams, error) {
+func (T Dialer) Dial() (*fed.Conn, error) {
 	c, err := net.Dial(T.Network, T.Address)
 	if err != nil {
-		return nil, backends.AcceptParams{}, err
-	}
-	conn := fed.WrapNetConn(c)
-	ctx := backends.AcceptContext{
-		Conn:    conn,
-		Options: T.AcceptOptions,
+		return nil, err
 	}
-	params, err := backends.Accept(&ctx)
+	conn := fed.NewConn(
+		fed.NewNetConn(c),
+	)
+	conn.User = T.Username
+	conn.Database = T.Database
+	err = backends.Accept(
+		conn,
+		T.SSLMode,
+		T.SSLConfig,
+		T.Username,
+		T.Credentials,
+		T.Database,
+		T.StartupParameters,
+	)
 	if err != nil {
-		return nil, backends.AcceptParams{}, err
+		return nil, err
 	}
-	return conn, params, nil
+	return conn, nil
 }
 
-func (T Dialer) Cancel(key [8]byte) error {
+func (T Dialer) Cancel(key [8]byte) {
 	c, err := net.Dial(T.Network, T.Address)
 	if err != nil {
-		return err
+		return
 	}
-	conn := fed.WrapNetConn(c)
+	conn := fed.NewConn(
+		fed.NewNetConn(c),
+	)
 	defer func() {
 		_ = conn.Close()
 	}()
 	if err = backends.Cancel(conn, key); err != nil {
-		return err
+		return
 	}
 
 	// wait for server to close the connection, this means that the server received it ok
-	_, err = conn.ReadPacket(true, nil)
-	if err != nil && !errors.Is(err, io.EOF) {
-		return err
-	}
-	return nil
+	_, _ = conn.ReadPacket(true, nil)
 }
diff --git a/lib/gat/pool/recipe/recipe.go b/lib/gat/pool/recipe/recipe.go
index b535ab40085da5e982857cb78e2c3d13d299c46a..aeb25e76eb3a4a81b6c4061df7a615274ffe8b7e 100644
--- a/lib/gat/pool/recipe/recipe.go
+++ b/lib/gat/pool/recipe/recipe.go
@@ -3,20 +3,19 @@ package recipe
 import (
 	"sync"
 
-	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
 	"gfx.cafe/gfx/pggat/lib/fed"
 )
 
 type Recipe struct {
-	options Options
+	config Config
 
 	count int
 	mu    sync.Mutex
 }
 
-func NewRecipe(options Options) *Recipe {
+func NewRecipe(config Config) *Recipe {
 	return &Recipe{
-		options: options,
+		config: config,
 	}
 }
 
@@ -24,12 +23,12 @@ func (T *Recipe) AllocateInitial() int {
 	T.mu.Lock()
 	defer T.mu.Unlock()
 
-	if T.count >= T.options.MinConnections {
+	if T.count >= T.config.MinConnections {
 		return 0
 	}
 
-	amount := T.options.MinConnections - T.count
-	T.count = T.options.MinConnections
+	amount := T.config.MinConnections - T.count
+	T.count = T.config.MinConnections
 
 	return amount
 }
@@ -38,8 +37,8 @@ func (T *Recipe) Allocate() bool {
 	T.mu.Lock()
 	defer T.mu.Unlock()
 
-	if T.options.MaxConnections != 0 {
-		if T.count >= T.options.MaxConnections {
+	if T.config.MaxConnections != 0 {
+		if T.count >= T.config.MaxConnections {
 			return false
 		}
 	}
@@ -52,7 +51,7 @@ func (T *Recipe) TryFree() bool {
 	T.mu.Lock()
 	defer T.mu.Unlock()
 
-	if T.count <= T.options.MinConnections {
+	if T.count <= T.config.MinConnections {
 		return false
 	}
 
@@ -67,10 +66,10 @@ func (T *Recipe) Free() {
 	T.count--
 }
 
-func (T *Recipe) Dial() (fed.Conn, backends.AcceptParams, error) {
-	return T.options.Dialer.Dial()
+func (T *Recipe) Dial() (*fed.Conn, error) {
+	return T.config.Dialer.Dial()
 }
 
-func (T *Recipe) Cancel(key [8]byte) error {
-	return T.options.Dialer.Cancel(key)
+func (T *Recipe) Cancel(key [8]byte) {
+	T.config.Dialer.Cancel(key)
 }
diff --git a/lib/gat/pool/scaler.go b/lib/gat/pool/scaler.go
index 3e3c57012e0233ee4b1502d8778d269d86d9274d..8beb64d3097d341369f26858cdb507dfb0bfb630 100644
--- a/lib/gat/pool/scaler.go
+++ b/lib/gat/pool/scaler.go
@@ -3,7 +3,7 @@ package pool
 import (
 	"time"
 
-	"tuxpa.in/a/zlog/log"
+	"go.uber.org/zap"
 )
 
 type scaler struct {
@@ -20,11 +20,11 @@ type scaler struct {
 func newScaler(pool *Pool) *scaler {
 	s := &scaler{
 		pool:    pool,
-		backoff: pool.options.ServerReconnectInitialTime,
+		backoff: pool.config.ServerReconnectInitialTime.Duration(),
 	}
 
-	if pool.options.ServerIdleTimeout != 0 {
-		s.idle = time.NewTimer(pool.options.ServerIdleTimeout)
+	if pool.config.ServerIdleTimeout != 0 {
+		s.idle = time.NewTimer(pool.config.ServerIdleTimeout.Duration())
 	}
 
 	return s
@@ -36,14 +36,14 @@ func (T *scaler) idleTimeout(now time.Time) {
 
 	var idlest *pooledServer
 	var idleStart time.Time
-	for idlest, idleStart = T.pool.idlest(); idlest != nil && now.Sub(idleStart) > T.pool.options.ServerIdleTimeout; idlest, idleStart = T.pool.idlest() {
+	for idlest, idleStart = T.pool.idlest(); idlest != nil && now.Sub(idleStart) > T.pool.config.ServerIdleTimeout.Duration(); idlest, idleStart = T.pool.idlest() {
 		T.pool.removeServer(idlest)
 	}
 
 	if idlest == nil {
-		wait = T.pool.options.ServerIdleTimeout
+		wait = T.pool.config.ServerIdleTimeout.Duration()
 	} else {
-		wait = idleStart.Add(T.pool.options.ServerIdleTimeout).Sub(now)
+		wait = idleStart.Add(T.pool.config.ServerIdleTimeout.Duration()).Sub(now)
 	}
 
 	T.idle.Reset(wait)
@@ -52,8 +52,8 @@ func (T *scaler) idleTimeout(now time.Time) {
 func (T *scaler) pendingTimeout() {
 	if T.backingOff {
 		T.backoff *= 2
-		if T.pool.options.ServerReconnectMaxTime != 0 && T.backoff > T.pool.options.ServerReconnectMaxTime {
-			T.backoff = T.pool.options.ServerReconnectMaxTime
+		if T.pool.config.ServerReconnectMaxTime != 0 && T.backoff > T.pool.config.ServerReconnectMaxTime.Duration() {
+			T.backoff = T.pool.config.ServerReconnectMaxTime.Duration()
 		}
 	}
 
@@ -61,14 +61,14 @@ func (T *scaler) pendingTimeout() {
 		// pending loop for scaling up
 		if T.pool.scaleUp() {
 			// scale up successful, see if we need to scale up more
-			T.backoff = T.pool.options.ServerReconnectInitialTime
+			T.backoff = T.pool.config.ServerReconnectInitialTime.Duration()
 			T.backingOff = false
 			continue
 		}
 
 		if T.backoff == 0 {
 			// no backoff
-			T.backoff = T.pool.options.ServerReconnectInitialTime
+			T.backoff = T.pool.config.ServerReconnectInitialTime.Duration()
 			T.backingOff = false
 			continue
 		}
@@ -80,7 +80,7 @@ func (T *scaler) pendingTimeout() {
 			T.pending.Reset(T.backoff)
 		}
 
-		log.Printf("failed to dial server. trying again in %v", T.backoff)
+		T.pool.config.Logger.Warn("failed to dial server", zap.Duration("backoff", T.backoff))
 
 		return
 	}
diff --git a/lib/gat/pool/server.go b/lib/gat/pool/server.go
index 5c250ee7ea5c014600c96a66dfacc7247be32321..9afa7c994359bd6157c0df746dc368efd1c6500c 100644
--- a/lib/gat/pool/server.go
+++ b/lib/gat/pool/server.go
@@ -2,11 +2,8 @@ package pool
 
 import (
 	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/middleware"
-	"gfx.cafe/gfx/pggat/lib/middleware/interceptor"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/eqp"
-	"gfx.cafe/gfx/pggat/lib/middleware/middlewares/ps"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/eqp"
+	"gfx.cafe/gfx/pggat/lib/fed/middlewares/ps"
 )
 
 type pooledServer struct {
@@ -19,40 +16,27 @@ type pooledServer struct {
 }
 
 func newServer(
-	options Options,
+	options Config,
 	recipe string,
-	conn fed.Conn,
-	initialParameters map[strutil.CIString]string,
-	backendKey [8]byte,
+	conn *fed.Conn,
 ) *pooledServer {
-	var middlewares []middleware.Middleware
-
 	var psServer *ps.Server
 	if options.ParameterStatusSync == ParameterStatusSyncDynamic {
 		// add ps middleware
-		psServer = ps.NewServer(initialParameters)
-		middlewares = append(middlewares, psServer)
+		psServer = ps.NewServer(conn.InitialParameters)
+		conn.Middleware = append(conn.Middleware, psServer)
 	}
 
 	var eqpServer *eqp.Server
 	if options.ExtendedQuerySync {
 		// add eqp middleware
 		eqpServer = eqp.NewServer()
-		middlewares = append(middlewares, eqpServer)
-	}
-
-	if len(middlewares) > 0 {
-		conn = interceptor.NewInterceptor(
-			conn,
-			middlewares...,
-		)
+		conn.Middleware = append(conn.Middleware, eqpServer)
 	}
 
 	return &pooledServer{
 		pooledConn: makeConn(
 			conn,
-			initialParameters,
-			backendKey,
 		),
 		recipe: recipe,
 		ps:     psServer,
diff --git a/lib/gat/pool/withcredentials.go b/lib/gat/pool/withcredentials.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c7382b380788ed36b6df39d241f3b5498e50493
--- /dev/null
+++ b/lib/gat/pool/withcredentials.go
@@ -0,0 +1,8 @@
+package pool
+
+import "gfx.cafe/gfx/pggat/lib/auth"
+
+type WithCredentials struct {
+	*Pool
+	Credentials auth.Credentials
+}
diff --git a/lib/gat/pooler.go b/lib/gat/pooler.go
new file mode 100644
index 0000000000000000000000000000000000000000..39d130621821146ecaaff5b708f35c4285ea913b
--- /dev/null
+++ b/lib/gat/pooler.go
@@ -0,0 +1,5 @@
+package gat
+
+type Pooler interface {
+	NewPool() *Pool
+}
diff --git a/lib/gat/poolers/session/module.go b/lib/gat/poolers/session/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..04d65b7121f162820295fb6f2a86a0ac30247946
--- /dev/null
+++ b/lib/gat/poolers/session/module.go
@@ -0,0 +1,54 @@
+package session
+
+import (
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/pool"
+)
+
+var PoolingOptions = pool.PoolingConfig{
+	NewPooler:               NewPooler,
+	ReleaseAfterTransaction: false,
+	ParameterStatusSync:     pool.ParameterStatusSyncInitial,
+	ExtendedQuerySync:       false,
+}
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	pool.ManagementConfig
+
+	log *zap.Logger
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.poolers.session",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+	return nil
+}
+
+func (T *Module) NewPool() *gat.Pool {
+	return pool.NewPool(pool.Config{
+		PoolingConfig: PoolingOptions,
+
+		ManagementConfig: T.ManagementConfig,
+
+		Logger: T.log,
+	})
+}
+
+var _ gat.Pooler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
+var _ caddy.Provisioner = (*Module)(nil)
diff --git a/lib/gat/pool/pools/session/pooler.go b/lib/gat/poolers/session/pooler.go
similarity index 87%
rename from lib/gat/pool/pools/session/pooler.go
rename to lib/gat/poolers/session/pooler.go
index 9827727ef102575e655e5580d1bf7bb8a79dac35..8f0dd004ce3ab9caf066e670325726eaf8508f23 100644
--- a/lib/gat/pool/pools/session/pooler.go
+++ b/lib/gat/poolers/session/pooler.go
@@ -13,6 +13,7 @@ type Pooler struct {
 	queue   []uuid.UUID
 	servers map[uuid.UUID]struct{}
 	ready   *sync.Cond
+	closed  bool
 	mu      sync.Mutex
 }
 
@@ -56,6 +57,10 @@ func (T *Pooler) TryAcquire() uuid.UUID {
 	T.mu.Lock()
 	defer T.mu.Unlock()
 
+	if T.closed {
+		return uuid.Nil
+	}
+
 	if len(T.queue) == 0 {
 		return uuid.Nil
 	}
@@ -69,6 +74,10 @@ func (T *Pooler) AcquireBlocking() uuid.UUID {
 	T.mu.Lock()
 	defer T.mu.Unlock()
 
+	if T.closed {
+		return uuid.Nil
+	}
+
 	for len(T.queue) == 0 {
 		if T.ready == nil {
 			T.ready = sync.NewCond(&T.mu)
@@ -76,6 +85,10 @@ func (T *Pooler) AcquireBlocking() uuid.UUID {
 		T.ready.Wait()
 	}
 
+	if T.closed {
+		return uuid.Nil
+	}
+
 	server := T.queue[len(T.queue)-1]
 	T.queue = T.queue[:len(T.queue)-1]
 	return server
@@ -104,4 +117,14 @@ func (T *Pooler) Release(server uuid.UUID) {
 	T.queue = append(T.queue, server)
 }
 
+func (T *Pooler) Close() {
+	T.mu.Lock()
+	defer T.mu.Unlock()
+
+	T.closed = true
+	if T.ready != nil {
+		T.ready.Broadcast()
+	}
+}
+
 var _ pool.Pooler = (*Pooler)(nil)
diff --git a/lib/gat/poolers/transaction/module.go b/lib/gat/poolers/transaction/module.go
new file mode 100644
index 0000000000000000000000000000000000000000..95a06b3686f607e8dfe95101d4e4c4e9854cb04f
--- /dev/null
+++ b/lib/gat/poolers/transaction/module.go
@@ -0,0 +1,53 @@
+package transaction
+
+import (
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+	"gfx.cafe/gfx/pggat/lib/gat/pool"
+)
+
+var PoolingOptions = pool.PoolingConfig{
+	NewPooler:               NewPooler,
+	ReleaseAfterTransaction: true,
+	ParameterStatusSync:     pool.ParameterStatusSyncDynamic,
+	ExtendedQuerySync:       true,
+}
+
+func init() {
+	caddy.RegisterModule((*Module)(nil))
+}
+
+type Module struct {
+	pool.ManagementConfig
+
+	log *zap.Logger
+}
+
+func (T *Module) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.poolers.transaction",
+		New: func() caddy.Module {
+			return new(Module)
+		},
+	}
+}
+
+func (T *Module) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+	return nil
+}
+
+func (T *Module) NewPool() *gat.Pool {
+	return pool.NewPool(pool.Config{
+		PoolingConfig: PoolingOptions,
+
+		ManagementConfig: T.ManagementConfig,
+
+		Logger: T.log,
+	})
+}
+
+var _ gat.Pooler = (*Module)(nil)
+var _ caddy.Module = (*Module)(nil)
diff --git a/lib/gat/pool/pools/transaction/pooler.go b/lib/gat/poolers/transaction/pooler.go
similarity index 95%
rename from lib/gat/pool/pools/transaction/pooler.go
rename to lib/gat/poolers/transaction/pooler.go
index 939718a29a4e27775c132783a011859709f69879..3eb1b9e1ca36403a498467ad86a42a86ad1b1b1b 100644
--- a/lib/gat/pool/pools/transaction/pooler.go
+++ b/lib/gat/poolers/transaction/pooler.go
@@ -47,4 +47,8 @@ func (T *Pooler) Release(server uuid.UUID) {
 	T.s.Release(server)
 }
 
+func (T *Pooler) Close() {
+	T.s.Close()
+}
+
 var _ pool.Pooler = (*Pooler)(nil)
diff --git a/lib/gat/provider.go b/lib/gat/provider.go
deleted file mode 100644
index 19d3662b7f26bff5a970435a5ea4846584f4fe15..0000000000000000000000000000000000000000
--- a/lib/gat/provider.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package gat
-
-import "gfx.cafe/gfx/pggat/lib/gat/metrics"
-
-// Provider provides pool to the server
-type Provider interface {
-	Module
-
-	Lookup(user, database string) *Pool
-	ReadMetrics(metrics *metrics.Pools)
-}
diff --git a/lib/gat/route.go b/lib/gat/route.go
new file mode 100644
index 0000000000000000000000000000000000000000..dc8e97fb7497300b57673f1f38a030435b126121
--- /dev/null
+++ b/lib/gat/route.go
@@ -0,0 +1,40 @@
+package gat
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/caddyserver/caddy/v2"
+)
+
+type RouteConfig struct {
+	Match  json.RawMessage `json:"match,omitempty" caddy:"namespace=pggat.matchers inline_key=matcher"`
+	Handle json.RawMessage `json:"handle,omitempty" caddy:"namespace=pggat.handlers inline_key=handler"`
+}
+
+type Route struct {
+	RouteConfig
+
+	match  Matcher
+	handle Handler
+}
+
+func (T *Route) Provision(ctx caddy.Context) error {
+	if T.Match != nil {
+		val, err := ctx.LoadModule(T, "Match")
+		if err != nil {
+			return fmt.Errorf("loading matcher module: %v", err)
+		}
+		T.match = val.(Matcher)
+	}
+	if T.Handle != nil {
+		val, err := ctx.LoadModule(T, "Handle")
+		if err != nil {
+			return fmt.Errorf("loading handle module: %v", err)
+		}
+		T.handle = val.(Handler)
+	}
+	return nil
+}
+
+var _ caddy.Provisioner = (*Route)(nil)
diff --git a/lib/gat/server.go b/lib/gat/server.go
index 8c4d587e03af8969691f4bb673a3a760f82cfb54..d31a0f6d593725e6214bbc6a9c98772a21d16a9d 100644
--- a/lib/gat/server.go
+++ b/lib/gat/server.go
@@ -1,174 +1,193 @@
 package gat
 
 import (
+	"crypto/tls"
 	"errors"
+	"fmt"
 	"io"
 	"net"
 
-	"tuxpa.in/a/zlog/log"
+	"github.com/caddyserver/caddy/v2"
+	"go.uber.org/zap"
 
 	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
 	"gfx.cafe/gfx/pggat/lib/fed"
+	packets "gfx.cafe/gfx/pggat/lib/fed/packets/v3.0"
 	"gfx.cafe/gfx/pggat/lib/gat/metrics"
-	"gfx.cafe/gfx/pggat/lib/util/beforeexit"
-	"gfx.cafe/gfx/pggat/lib/util/flip"
-	"gfx.cafe/gfx/pggat/lib/util/maps"
+	"gfx.cafe/gfx/pggat/lib/perror"
 )
 
+type ServerConfig struct {
+	Listen []ListenerConfig `json:"listen,omitempty"`
+	Routes []RouteConfig    `json:"routes,omitempty"`
+}
+
 type Server struct {
-	modules   []Module
-	providers []Provider
-	listeners []Listener
+	ServerConfig
 
-	keys maps.RWLocked[[8]byte, *Pool]
+	listen              []*Listener
+	routes              []*Route
+	cancellableHandlers []CancellableHandler
+	metricsHandlers     []MetricsHandler
+
+	log *zap.Logger
 }
 
-func (T *Server) AddModule(module Module) {
-	T.modules = append(T.modules, module)
-	if provider, ok := module.(Provider); ok {
-		T.providers = append(T.providers, provider)
+func (T *Server) Provision(ctx caddy.Context) error {
+	T.log = ctx.Logger()
+
+	T.listen = make([]*Listener, 0, len(T.Listen))
+	for _, config := range T.Listen {
+		listener := &Listener{
+			ListenerConfig: config,
+		}
+		if err := listener.Provision(ctx); err != nil {
+			return err
+		}
+		T.listen = append(T.listen, listener)
 	}
-	if listener, ok := module.(Listener); ok {
-		T.listeners = append(T.listeners, listener)
+
+	T.routes = make([]*Route, 0, len(T.Routes))
+	for _, config := range T.Routes {
+		route := &Route{
+			RouteConfig: config,
+		}
+		if err := route.Provision(ctx); err != nil {
+			return err
+		}
+		if cancellableHandler, ok := route.handle.(CancellableHandler); ok {
+			T.cancellableHandlers = append(T.cancellableHandlers, cancellableHandler)
+		}
+		if metricsHandler, ok := route.handle.(MetricsHandler); ok {
+			T.metricsHandlers = append(T.metricsHandlers, metricsHandler)
+		}
+		T.routes = append(T.routes, route)
 	}
+
+	return nil
 }
 
-func (T *Server) cancel(key [8]byte) error {
-	p, ok := T.keys.Load(key)
-	if !ok {
-		return nil
+func (T *Server) Start() error {
+	for _, listener := range T.listen {
+		if err := listener.Start(); err != nil {
+			return err
+		}
+
+		go func(listener *Listener) {
+			for {
+				if !T.acceptFrom(listener) {
+					break
+				}
+			}
+		}(listener)
 	}
 
-	return p.Cancel(key)
+	return nil
 }
 
-func (T *Server) lookupPool(user, database string) *Pool {
-	for _, provider := range T.providers {
-		p := provider.Lookup(user, database)
-		if p != nil {
-			return p
+func (T *Server) Stop() error {
+	for _, listen := range T.listen {
+		if err := listen.Stop(); err != nil {
+			return err
 		}
 	}
 
 	return nil
 }
 
-func (T *Server) registerKey(key [8]byte, p *Pool) {
-	T.keys.Store(key, p)
+func (T *Server) Cancel(key [8]byte) {
+	for _, cancellableHandler := range T.cancellableHandlers {
+		cancellableHandler.Cancel(key)
+	}
 }
 
-func (T *Server) unregisterKey(key [8]byte) {
-	T.keys.Delete(key)
+func (T *Server) ReadMetrics(m *metrics.Server) {
+	for _, metricsHandler := range T.metricsHandlers {
+		metricsHandler.ReadMetrics(&m.Handler)
+	}
 }
 
-func (T *Server) serve(conn fed.Conn, params frontends.AcceptParams) error {
-	if params.CancelKey != [8]byte{} {
-		return T.cancel(params.CancelKey)
-	}
+func (T *Server) Serve(conn *fed.Conn) {
+	for _, route := range T.routes {
+		if route.match != nil && !route.match.Matches(conn) {
+			continue
+		}
 
-	p := T.lookupPool(params.User, params.Database)
-	if p == nil {
-		return errPoolNotFound{
-			User:     params.User,
-			Database: params.Database,
+		if route.handle == nil {
+			continue
 		}
-	}
+		err := route.handle.Handle(conn)
+		if err != nil {
+			if errors.Is(err, io.EOF) {
+				// normal closure
+				return
+			}
 
-	ctx := frontends.AuthenticateContext{
-		Conn: conn,
-		Options: frontends.AuthenticateOptions{
-			Credentials: p.GetCredentials(),
-		},
-	}
-	auth, err := frontends.Authenticate(&ctx)
-	if err != nil {
-		return err
+			errResp := packets.ErrorResponse{
+				Error: perror.Wrap(err),
+			}
+			_ = conn.WritePacket(errResp.IntoPacket(nil))
+			return
+		}
 	}
 
-	T.registerKey(auth.BackendKey, p)
-	defer T.unregisterKey(auth.BackendKey)
-
-	return p.Serve(conn, params.InitialParameters, auth.BackendKey)
+	// database not found
+	errResp := packets.ErrorResponse{
+		Error: perror.New(
+			perror.FATAL,
+			perror.InvalidPassword,
+			fmt.Sprintf(`Database "%s" not found`, conn.Database),
+		),
+	}
+	_ = conn.WritePacket(errResp.IntoPacket(nil))
+	T.log.Warn("database not found", zap.String("user", conn.User), zap.String("database", conn.Database))
 }
 
-func (T *Server) accept(raw net.Conn, acceptOptions FrontendAcceptOptions) {
-	conn := fed.WrapNetConn(raw)
-
+func (T *Server) accept(listener *Listener, conn *fed.Conn) {
 	defer func() {
 		_ = conn.Close()
 	}()
 
-	ctx := frontends.AcceptContext{
-		Conn:    conn,
-		Options: acceptOptions,
-	}
-	params, err2 := frontends.Accept(&ctx)
-	if err2 != nil {
-		log.Print("error accepting client: ", err2)
-		return
+	var tlsConfig *tls.Config
+	if listener.ssl != nil {
+		tlsConfig = listener.ssl.ServerTLSConfig()
 	}
 
-	err := T.serve(conn, params)
-	if err != nil && !errors.Is(err, io.EOF) {
-		log.Print("error serving client: ", err)
+	var cancelKey [8]byte
+	var isCanceling bool
+	var err error
+	cancelKey, isCanceling, err = frontends.Accept(conn, tlsConfig)
+	if err != nil {
+		T.log.Warn("error accepting client", zap.Error(err))
 		return
 	}
-}
 
-func (T *Server) Listen(network, address string) (net.Listener, error) {
-	listener, err := net.Listen(network, address)
-	if err != nil {
-		return nil, err
-	}
-	if network == "unix" {
-		beforeexit.Run(func() {
-			_ = listener.Close()
-		})
+	if isCanceling {
+		T.Cancel(cancelKey)
+		return
 	}
 
-	log.Printf("listening on %s(%s)", network, address)
-
-	return listener, nil
+	T.Serve(conn)
 }
 
-func (T *Server) Serve(listener net.Listener, acceptOptions FrontendAcceptOptions) error {
-	for {
-		raw, err := listener.Accept()
-		if err != nil {
-			if errors.Is(err, net.ErrClosed) {
-				break
-			}
+func (T *Server) acceptFrom(listener *Listener) bool {
+	conn, err := listener.accept()
+	if err != nil {
+		if errors.Is(err, net.ErrClosed) {
+			return false
 		}
-
-		go T.accept(raw, acceptOptions)
-	}
-
-	return nil
-}
-
-func (T *Server) ListenAndServe() error {
-	var b flip.Bank
-
-	if len(T.listeners) > 0 {
-		l := T.listeners[0]
-		endpoints := l.Endpoints()
-		for _, endpoint := range endpoints {
-			e := endpoint
-			b.Queue(func() error {
-				listener, err := T.Listen(e.Network, e.Address)
-				if err != nil {
-					return err
-				}
-				return T.Serve(listener, e.AcceptOptions)
-			})
+		if netErr, ok := err.(*net.OpError); ok {
+			// why can't they just expose this error
+			if netErr.Err.Error() == "listener 'closed' 😉" {
+				return false
+			}
 		}
+		T.log.Warn("error accepting client", zap.Error(err))
+		return true
 	}
 
-	return b.Wait()
+	go T.accept(listener, conn)
+	return true
 }
 
-func (T *Server) ReadMetrics(m *metrics.Server) {
-	for _, provider := range T.providers {
-		provider.ReadMetrics(&m.Pools)
-	}
-}
+var _ caddy.Provisioner = (*Server)(nil)
diff --git a/lib/gat/ssl.go b/lib/gat/ssl.go
new file mode 100644
index 0000000000000000000000000000000000000000..71e6289add250b1f7eaa5f0529aff0459a433a26
--- /dev/null
+++ b/lib/gat/ssl.go
@@ -0,0 +1,11 @@
+package gat
+
+import "crypto/tls"
+
+type SSLServer interface {
+	ServerTLSConfig() *tls.Config
+}
+
+type SSLClient interface {
+	ClientTLSConfig() *tls.Config
+}
diff --git a/lib/gat/ssl/clients/insecure_skip_verify/client.go b/lib/gat/ssl/clients/insecure_skip_verify/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..e1c1b3519f1eded4b5a724133fb142771659d346
--- /dev/null
+++ b/lib/gat/ssl/clients/insecure_skip_verify/client.go
@@ -0,0 +1,34 @@
+package insecure_skip_verify
+
+import (
+	"crypto/tls"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+func init() {
+	caddy.RegisterModule((*Client)(nil))
+}
+
+type Client struct {
+}
+
+func (T *Client) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.ssl.clients.insecure_skip_verify",
+		New: func() caddy.Module {
+			return new(Client)
+		},
+	}
+}
+
+func (T *Client) ClientTLSConfig() *tls.Config {
+	return &tls.Config{
+		InsecureSkipVerify: true,
+	}
+}
+
+var _ gat.SSLClient = (*Client)(nil)
+var _ caddy.Module = (*Client)(nil)
diff --git a/lib/gat/modules/ssl_endpoint/module.go b/lib/gat/ssl/servers/self_signed/server.go
similarity index 53%
rename from lib/gat/modules/ssl_endpoint/module.go
rename to lib/gat/ssl/servers/self_signed/server.go
index 4589cd98db87ad8eac08d75f3fc88c4034ac2c20..7dec991c5ef194c66f40a16fe9459e03653c2dc2 100644
--- a/lib/gat/modules/ssl_endpoint/module.go
+++ b/lib/gat/ssl/servers/self_signed/server.go
@@ -1,4 +1,4 @@
-package ssl_endpoint
+package self_signed
 
 import (
 	"crypto/rand"
@@ -10,25 +10,33 @@ import (
 	"net"
 	"time"
 
-	"tuxpa.in/a/zlog/log"
+	"github.com/caddyserver/caddy/v2"
 
 	"gfx.cafe/gfx/pggat/lib/gat"
-	"gfx.cafe/gfx/pggat/lib/util/strutil"
 )
 
-type Module struct {
-	config *tls.Config
+func init() {
+	caddy.RegisterModule((*Server)(nil))
 }
 
-func NewModule() (*Module, error) {
-	return &Module{}, nil
+type Server struct {
+	tlsConfig *tls.Config
 }
 
-func (T *Module) generateKeys() error {
+func (T *Server) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.ssl.servers.self_signed",
+		New: func() caddy.Module {
+			return new(Server)
+		},
+	}
+}
+
+func (T *Server) signCert() (tls.Certificate, error) {
 	// generate private key
 	priv, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
-		return err
+		return tls.Certificate{}, err
 	}
 
 	keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
@@ -39,7 +47,7 @@ func (T *Module) generateKeys() error {
 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
 	if err != nil {
-		return err
+		return tls.Certificate{}, err
 	}
 
 	template := x509.Certificate{
@@ -54,20 +62,27 @@ func (T *Module) generateKeys() error {
 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
 		BasicConstraintsValid: true,
 	}
-
 	// TODO(garet)
-	template.IPAddresses = append(template.IPAddresses, net.ParseIP("192.168.1.1"))
+	template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1"))
 
 	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
 	if err != nil {
-		return err
+		return tls.Certificate{}, err
 	}
 
 	var cert tls.Certificate
 	cert.PrivateKey = priv
 	cert.Certificate = append(cert.Certificate, derBytes)
 
-	T.config = &tls.Config{
+	return cert, nil
+}
+
+func (T *Server) Provision(ctx caddy.Context) error {
+	cert, err := T.signCert()
+	if err != nil {
+		return err
+	}
+	T.tlsConfig = &tls.Config{
 		Certificates: []tls.Certificate{
 			cert,
 		},
@@ -75,35 +90,10 @@ func (T *Module) generateKeys() error {
 	return nil
 }
 
-func (T *Module) GatModule() {}
-
-func (T *Module) Endpoints() []gat.Endpoint {
-	if T.config == nil {
-		if err := T.generateKeys(); err != nil {
-			log.Printf("failed to generate ssl certificate: %v", err)
-		}
-	}
-
-	return []gat.Endpoint{
-		{
-			Network: "tcp",
-			Address: ":5432",
-			AcceptOptions: gat.FrontendAcceptOptions{
-				SSLRequired: false,
-				SSLConfig:   T.config,
-				AllowedStartupOptions: []strutil.CIString{
-					strutil.MakeCIString("client_encoding"),
-					strutil.MakeCIString("datestyle"),
-					strutil.MakeCIString("timezone"),
-					strutil.MakeCIString("standard_conforming_strings"),
-					strutil.MakeCIString("application_name"),
-					strutil.MakeCIString("extra_float_digits"),
-					strutil.MakeCIString("options"),
-				},
-			},
-		},
-	}
+func (T *Server) ServerTLSConfig() *tls.Config {
+	return T.tlsConfig
 }
 
-var _ gat.Module = (*Module)(nil)
-var _ gat.Listener = (*Module)(nil)
+var _ gat.SSLServer = (*Server)(nil)
+var _ caddy.Module = (*Server)(nil)
+var _ caddy.Provisioner = (*Server)(nil)
diff --git a/lib/gat/ssl/servers/x509_key_pair/server.go b/lib/gat/ssl/servers/x509_key_pair/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..905e5d995ac95bf17a2068b0ee2f6be78b3cb927
--- /dev/null
+++ b/lib/gat/ssl/servers/x509_key_pair/server.go
@@ -0,0 +1,47 @@
+package x509_key_pair
+
+import (
+	"crypto/tls"
+
+	"github.com/caddyserver/caddy/v2"
+
+	"gfx.cafe/gfx/pggat/lib/gat"
+)
+
+type Server struct {
+	CertFile string `json:"cert_file"`
+	KeyFile  string `json:"key_file"`
+
+	tlsConfig *tls.Config
+}
+
+func (T *Server) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID: "pggat.ssl.servers.x509_key_pair",
+		New: func() caddy.Module {
+			return new(Server)
+		},
+	}
+}
+
+func (T *Server) Provision(ctx caddy.Context) error {
+	cert, err := tls.LoadX509KeyPair(T.CertFile, T.KeyFile)
+	if err != nil {
+		return err
+	}
+
+	T.tlsConfig = &tls.Config{
+		Certificates: []tls.Certificate{
+			cert,
+		},
+	}
+	return nil
+}
+
+func (T *Server) ServerTLSConfig() *tls.Config {
+	return T.tlsConfig
+}
+
+var _ gat.SSLServer = (*Server)(nil)
+var _ caddy.Module = (*Server)(nil)
+var _ caddy.Provisioner = (*Server)(nil)
diff --git a/lib/gat/standard/standard.go b/lib/gat/standard/standard.go
new file mode 100644
index 0000000000000000000000000000000000000000..507ea27b9b33812887888db9bc49a3080ef3b38e
--- /dev/null
+++ b/lib/gat/standard/standard.go
@@ -0,0 +1,40 @@
+package standard
+
+import (
+	// base server
+	_ "gfx.cafe/gfx/pggat/lib/gat"
+
+	// matchers
+	_ "gfx.cafe/gfx/pggat/lib/gat/matchers"
+
+	// ssl servers
+	_ "gfx.cafe/gfx/pggat/lib/gat/ssl/servers/self_signed"
+	_ "gfx.cafe/gfx/pggat/lib/gat/ssl/servers/x509_key_pair"
+
+	// ssl clients
+	_ "gfx.cafe/gfx/pggat/lib/gat/ssl/clients/insecure_skip_verify"
+
+	// middlewares
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/allowed_startup_parameters"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/error"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/require_ssl"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_database"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_parameter"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_password"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_user"
+
+	// handlers
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/discovery"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/pgbouncer_spilo"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/pool"
+
+	// discovery
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/discovery/discoverers/digitalocean"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/discovery/discoverers/google_cloud_sql"
+	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/discovery/discoverers/zalando_operator"
+
+	// poolers
+	_ "gfx.cafe/gfx/pggat/lib/gat/poolers/session"
+	_ "gfx.cafe/gfx/pggat/lib/gat/poolers/transaction"
+)
diff --git a/lib/gsql/addr.go b/lib/gsql/addr.go
new file mode 100644
index 0000000000000000000000000000000000000000..61bcf5d2439817290ec46ef99a1ed926d9003004
--- /dev/null
+++ b/lib/gsql/addr.go
@@ -0,0 +1,15 @@
+package gsql
+
+import "net"
+
+type Addr struct{}
+
+func (Addr) Network() string {
+	return "gsql"
+}
+
+func (Addr) String() string {
+	return "local gsql client"
+}
+
+var _ net.Addr = Addr{}
diff --git a/lib/gsql/client.go b/lib/gsql/client.go
index 85b5d2d9f81bf72dd2a81b01a7c3dd34a32a9cdc..de0a0630725fab6bf7b678fe490196a813a992f3 100644
--- a/lib/gsql/client.go
+++ b/lib/gsql/client.go
@@ -143,4 +143,4 @@ func (T *Client) Close() error {
 	return nil
 }
 
-var _ fed.Conn = (*Client)(nil)
+var _ fed.ReadWriteCloser = (*Client)(nil)
diff --git a/lib/gsql/query_test.go b/lib/gsql/query_test.go
index 71ef6af29144e047ab26c7f349847c6a226d2b21..320bf4d5d048e267b97b92692472d56f84e83985 100644
--- a/lib/gsql/query_test.go
+++ b/lib/gsql/query_test.go
@@ -1,11 +1,10 @@
 package gsql
 
 import (
+	"log"
 	"net"
 	"testing"
 
-	"tuxpa.in/a/zlog/log"
-
 	"gfx.cafe/gfx/pggat/lib/auth/credentials"
 	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
 	"gfx.cafe/gfx/pggat/lib/bouncer/bouncers/v2"
@@ -24,19 +23,19 @@ func TestQuery(t *testing.T) {
 		t.Error(err)
 		return
 	}
-	server := fed.WrapNetConn(s)
-	ctx := backends.AcceptContext{
-		Conn: server,
-		Options: backends.AcceptOptions{
+	server := fed.NewConn(fed.NewNetConn(s))
+	err = backends.Accept(
+		server,
+		"",
+		nil,
+		"postgres",
+		credentials.Cleartext{
 			Username: "postgres",
-			Credentials: credentials.Cleartext{
-				Username: "postgres",
-				Password: "password",
-			},
-			Database: "postgres",
+			Password: "password",
 		},
-	}
-	_, err = backends.Accept(&ctx)
+		"postgres",
+		nil,
+	)
 	if err != nil {
 		t.Error(err)
 		return
@@ -59,7 +58,7 @@ func TestQuery(t *testing.T) {
 	if err != nil {
 		t.Error(err)
 	}
-	_, clientErr, serverErr := bouncers.Bounce(client, server, initial)
+	_, clientErr, serverErr := bouncers.Bounce(fed.NewConn(client), server, initial)
 	if clientErr != nil {
 		t.Error(clientErr)
 	}
diff --git a/lib/middleware/context.go b/lib/middleware/context.go
deleted file mode 100644
index be60aa0ee3fe8eb7f8ceaa6700e90b07f6f8dc9b..0000000000000000000000000000000000000000
--- a/lib/middleware/context.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package middleware
-
-import "gfx.cafe/gfx/pggat/lib/fed"
-
-type Context interface {
-	// Cancel the current packet
-	Cancel()
-
-	// Write packet to underlying connection
-	Write(packet fed.Packet) error
-}
diff --git a/lib/middleware/interceptor/context.go b/lib/middleware/interceptor/context.go
deleted file mode 100644
index ea4c7a767aa8d606fe282fbfd4e463803830fad0..0000000000000000000000000000000000000000
--- a/lib/middleware/interceptor/context.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package interceptor
-
-import (
-	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/middleware"
-	"gfx.cafe/gfx/pggat/lib/util/decorator"
-)
-
-type Context struct {
-	noCopy decorator.NoCopy
-
-	cancelled bool
-
-	// for normal Write / WriteUntyped
-	rw fed.ReadWriter
-}
-
-func makeContext(rw fed.ReadWriter) Context {
-	return Context{
-		rw: rw,
-	}
-}
-
-func (T *Context) reset() {
-	T.cancelled = false
-}
-
-func (T *Context) Cancel() {
-	T.cancelled = true
-}
-
-func (T *Context) Write(packet fed.Packet) error {
-	return T.rw.WritePacket(packet)
-}
-
-var _ middleware.Context = (*Context)(nil)
diff --git a/lib/middleware/interceptor/interceptor.go b/lib/middleware/interceptor/interceptor.go
deleted file mode 100644
index 3c13c928eca1e52bd615892d9c217e6006890c78..0000000000000000000000000000000000000000
--- a/lib/middleware/interceptor/interceptor.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package interceptor
-
-import (
-	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/middleware"
-)
-
-type Interceptor struct {
-	middlewares []middleware.Middleware
-	context     Context
-	rw          fed.Conn
-}
-
-func NewInterceptor(rw fed.Conn, middlewares ...middleware.Middleware) *Interceptor {
-	if v, ok := rw.(*Interceptor); ok {
-		v.middlewares = append(v.middlewares, middlewares...)
-		return v
-	}
-	return &Interceptor{
-		middlewares: middlewares,
-		context:     makeContext(rw),
-		rw:          rw,
-	}
-}
-
-func (T *Interceptor) ReadPacket(typed bool, packet fed.Packet) (fed.Packet, error) {
-outer:
-	for {
-		var err error
-		packet, err = T.rw.ReadPacket(typed, packet)
-		if err != nil {
-			return packet, err
-		}
-
-		for _, mw := range T.middlewares {
-			T.context.reset()
-			err = mw.Read(&T.context, packet)
-			if err != nil {
-				return packet, err
-			}
-			if T.context.cancelled {
-				continue outer
-			}
-		}
-
-		return packet, nil
-	}
-}
-
-func (T *Interceptor) WritePacket(packet fed.Packet) error {
-	for _, mw := range T.middlewares {
-		T.context.reset()
-		err := mw.Write(&T.context, packet)
-		if err != nil {
-			return err
-		}
-		if T.context.cancelled {
-			return nil
-		}
-	}
-
-	return T.rw.WritePacket(packet)
-}
-
-func (T *Interceptor) Close() error {
-	return T.rw.Close()
-}
-
-var _ fed.Conn = (*Interceptor)(nil)
diff --git a/lib/middleware/middleware.go b/lib/middleware/middleware.go
deleted file mode 100644
index 63f5401dad430cd2aa11782bc4c1914bd9de6a80..0000000000000000000000000000000000000000
--- a/lib/middleware/middleware.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package middleware
-
-import "gfx.cafe/gfx/pggat/lib/fed"
-
-type Middleware interface {
-	Read(ctx Context, packet fed.Packet) error
-	Write(ctx Context, packet fed.Packet) error
-}
diff --git a/lib/middleware/middlewares/eqp/client.go b/lib/middleware/middlewares/eqp/client.go
deleted file mode 100644
index 89175b71897dbab224e41f673f8700b52cd797d4..0000000000000000000000000000000000000000
--- a/lib/middleware/middlewares/eqp/client.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package eqp
-
-import (
-	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/middleware"
-)
-
-type Client struct {
-	state State
-}
-
-func NewClient() *Client {
-	return new(Client)
-}
-
-func (T *Client) Read(_ middleware.Context, packet fed.Packet) error {
-	T.state.C2S(packet)
-	return nil
-}
-
-func (T *Client) Write(_ middleware.Context, packet fed.Packet) error {
-	T.state.S2C(packet)
-	return nil
-}
-
-var _ middleware.Middleware = (*Client)(nil)
diff --git a/lib/middleware/middlewares/eqp/server.go b/lib/middleware/middlewares/eqp/server.go
deleted file mode 100644
index 04a11d178597a9c1bafdf33cd476282aeb62bb0b..0000000000000000000000000000000000000000
--- a/lib/middleware/middlewares/eqp/server.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package eqp
-
-import (
-	"gfx.cafe/gfx/pggat/lib/fed"
-	"gfx.cafe/gfx/pggat/lib/middleware"
-)
-
-type Server struct {
-	state State
-}
-
-func NewServer() *Server {
-	return new(Server)
-}
-
-func (T *Server) Read(_ middleware.Context, packet fed.Packet) error {
-	T.state.S2C(packet)
-	return nil
-}
-
-func (T *Server) Write(_ middleware.Context, packet fed.Packet) error {
-	T.state.C2S(packet)
-	return nil
-}
-
-var _ middleware.Middleware = (*Server)(nil)
diff --git a/lib/perror/wrap.go b/lib/perror/wrap.go
index 7a652d7b50c569f42fe02571ef8f4215a58c48ad..72e918deafadfe0782d1f315fcfd3dd3fac2327f 100644
--- a/lib/perror/wrap.go
+++ b/lib/perror/wrap.go
@@ -4,6 +4,9 @@ func Wrap(err error) Error {
 	if err == nil {
 		return nil
 	}
+	if perr, ok := err.(Error); ok {
+		return perr
+	}
 	return New(
 		FATAL,
 		InternalError,
diff --git a/lib/rob/scheduler.go b/lib/rob/scheduler.go
index 1cef414e6ffcaad16cb1b2a8526bf06ad0c6cf5b..97fcadd721142da07b537c7ac78b358bc0376ab1 100644
--- a/lib/rob/scheduler.go
+++ b/lib/rob/scheduler.go
@@ -28,4 +28,6 @@ type Scheduler interface {
 	// Release will release a worker.
 	// This should be called after acquire unless the worker is removed with RemoveWorker
 	Release(worker uuid.UUID)
+
+	Close()
 }
diff --git a/lib/rob/schedulers/v2/scheduler.go b/lib/rob/schedulers/v2/scheduler.go
index 2a5d72e6d3b3e385c514d6c1d574614738a8e07f..01443c79b9a36156e302cdc3b66e2bedd8d428cf 100644
--- a/lib/rob/schedulers/v2/scheduler.go
+++ b/lib/rob/schedulers/v2/scheduler.go
@@ -1,13 +1,15 @@
 package schedulers
 
 import (
+	"sync"
+
+	"github.com/google/uuid"
+
 	"gfx.cafe/gfx/pggat/lib/rob"
 	"gfx.cafe/gfx/pggat/lib/rob/schedulers/v2/job"
 	"gfx.cafe/gfx/pggat/lib/rob/schedulers/v2/sink"
 	"gfx.cafe/gfx/pggat/lib/util/maps"
 	"gfx.cafe/gfx/pggat/lib/util/pools"
-	"github.com/google/uuid"
-	"sync"
 )
 
 type Scheduler struct {
@@ -20,6 +22,7 @@ type Scheduler struct {
 	backlog []job.Stalled
 	bmu     sync.Mutex
 	sinks   map[uuid.UUID]*sink.Sink
+	closed  bool
 	mu      sync.RWMutex
 }
 
@@ -102,6 +105,10 @@ func (T *Scheduler) TryAcquire(j job.Concurrent) uuid.UUID {
 	T.mu.RLock()
 	defer T.mu.RUnlock()
 
+	if T.closed {
+		return uuid.Nil
+	}
+
 	return T.tryAcquire(j)
 }
 
@@ -130,6 +137,13 @@ func (T *Scheduler) Enqueue(j ...job.Stalled) {
 	T.mu.RLock()
 	defer T.mu.RUnlock()
 
+	if T.closed {
+		for _, jj := range j {
+			close(jj.Ready)
+			return
+		}
+	}
+
 	for _, jj := range j {
 		T.enqueue(jj)
 	}
@@ -146,7 +160,6 @@ func (T *Scheduler) Acquire(user uuid.UUID, mode rob.SyncMode) uuid.UUID {
 		if !ok {
 			ready = make(chan uuid.UUID, 1)
 		}
-		defer T.ready.Put(ready)
 
 		j := job.Stalled{
 			Concurrent: job.Concurrent{
@@ -156,7 +169,11 @@ func (T *Scheduler) Acquire(user uuid.UUID, mode rob.SyncMode) uuid.UUID {
 		}
 		T.Enqueue(j)
 
-		return <-ready
+		s, ok := <-ready
+		if ok {
+			T.ready.Put(ready)
+		}
+		return s
 	case rob.SyncModeTryNonBlocking:
 		if id := T.Acquire(user, rob.SyncModeNonBlocking); id != uuid.Nil {
 			return id
@@ -201,4 +218,27 @@ func (T *Scheduler) stealFor(worker uuid.UUID) {
 	}
 }
 
+func (T *Scheduler) Close() {
+	T.mu.Lock()
+	defer T.mu.Unlock()
+
+	T.closed = true
+
+	for worker, s := range T.sinks {
+		delete(T.sinks, worker)
+
+		// now we need to reschedule all the work that was scheduled to s (stalled only).
+		jobs := s.StealAll()
+
+		for _, j := range jobs {
+			close(j.Ready)
+		}
+	}
+
+	for _, j := range T.backlog {
+		close(j.Ready)
+	}
+	T.backlog = T.backlog[:0]
+}
+
 var _ rob.Scheduler = (*Scheduler)(nil)
diff --git a/lib/util/beforeexit/run.go b/lib/util/beforeexit/run.go
deleted file mode 100644
index 7dfc25c29aa889ca1dfe448721d05693c66b11ed..0000000000000000000000000000000000000000
--- a/lib/util/beforeexit/run.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package beforeexit
-
-import (
-	"os"
-	"os/signal"
-	"sync"
-	"syscall"
-)
-
-var (
-	q      []func()
-	active bool
-	mu     sync.Mutex
-)
-
-func registerHandler() {
-	c := make(chan os.Signal, 2)
-	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
-
-	go func() {
-		<-c
-
-		mu.Lock()
-		defer mu.Unlock()
-		for _, fn := range q {
-			// ignore any panics in funcs
-			func() {
-				defer func() {
-					recover()
-				}()
-				fn()
-			}()
-		}
-
-		os.Exit(1)
-	}()
-}
-
-// Run will register a func to run before exit on receiving an interrupt
-// Tasks will run in the order that they are added
-func Run(fn func()) {
-	mu.Lock()
-	defer mu.Unlock()
-
-	q = append(q, fn)
-	if !active {
-		active = true
-		registerHandler()
-	}
-}
diff --git a/lib/util/dur/duration.go b/lib/util/dur/duration.go
new file mode 100644
index 0000000000000000000000000000000000000000..b2847c62969a15466e6bb090276a0d0899b46bf7
--- /dev/null
+++ b/lib/util/dur/duration.go
@@ -0,0 +1,32 @@
+package dur
+
+import (
+	"encoding/json"
+	"time"
+)
+
+type Duration time.Duration
+
+func (T *Duration) Duration() time.Duration {
+	return time.Duration(*T)
+}
+
+func (T *Duration) UnmarshalJSON(bytes []byte) error {
+	// try as string
+	var str string
+	if err := json.Unmarshal(bytes, &str); err == nil {
+		*(*time.Duration)(T), err = time.ParseDuration(str)
+		return err
+	}
+
+	// try num
+	var num int64
+	if err := json.Unmarshal(bytes, &num); err != nil {
+		return err
+	}
+	*T = Duration(num)
+
+	return nil
+}
+
+var _ json.Unmarshaler = (*Duration)(nil)
diff --git a/lib/util/slices/remove.go b/lib/util/slices/remove.go
index 1848711f5065da4f78b972da1e0675d17ecd24bc..04ca24c193ccdf31c21187f0b9452dc78f865346 100644
--- a/lib/util/slices/remove.go
+++ b/lib/util/slices/remove.go
@@ -8,7 +8,12 @@ func Remove[T comparable](slice []T, item T) []T {
 	if i == -1 {
 		return slice
 	}
-	copy(slice[i:], slice[i+1:])
+	return RemoveIndex(slice, i)
+}
+
+func RemoveIndex[T any](slice []T, idx int) []T {
+	item := slice[idx]
+	copy(slice[idx:], slice[idx+1:])
 	slice[len(slice)-1] = item
 	return slice[:len(slice)-1]
 }
@@ -19,7 +24,11 @@ func Delete[T comparable](slice []T, item T) []T {
 	if i == -1 {
 		return slice
 	}
-	copy(slice[i:], slice[i+1:])
+	return DeleteIndex(slice, i)
+}
+
+func DeleteIndex[T any](slice []T, idx int) []T {
+	copy(slice[idx:], slice[idx+1:])
 	slice[len(slice)-1] = *new(T)
 	return slice[:len(slice)-1]
 }
diff --git a/lib/util/slices/sorted_test.go b/lib/util/slices/sorted_test.go
index 49e19e073bf939cd94dfaaed2f793cfde6936b31..bb8dd923b163bc59e0b42eb5ae9e271900a793ec 100644
--- a/lib/util/slices/sorted_test.go
+++ b/lib/util/slices/sorted_test.go
@@ -1,10 +1,9 @@
 package slices
 
 import (
+	"log"
 	"sort"
 	"testing"
-
-	"tuxpa.in/a/zlog/log"
 )
 
 func TestSorted_Insert(t *testing.T) {
diff --git a/lib/util/strutil/cut.go b/lib/util/strutil/cut.go
new file mode 100644
index 0000000000000000000000000000000000000000..d8798b33d244a0368bd6acd7b3557f2aa5dc3c57
--- /dev/null
+++ b/lib/util/strutil/cut.go
@@ -0,0 +1,21 @@
+package strutil
+
+import "strings"
+
+// CutLeft is similar to strings.Cut but it returns "", s, false if not found
+func CutLeft(s string, sep string) (before, after string, found bool) {
+	before, after, found = strings.Cut(s, sep)
+	if !found {
+		after = before
+		before = ""
+	}
+	return
+}
+
+// CutRight is similar to strings.Cut but it searches from the end first
+func CutRight(s string, sep string) (before, after string, found bool) {
+	if i := strings.LastIndex(s, sep); i >= 0 {
+		return s[:i], s[i+len(sep):], true
+	}
+	return s, "", false
+}
diff --git a/pggat.Dockerfile b/pggat.Dockerfile
index 57061010df72e342af9468b6d92544448a2b58e6..a9b723d6601493f19fb9466acc2a19a1e32b48d7 100644
--- a/pggat.Dockerfile
+++ b/pggat.Dockerfile
@@ -5,7 +5,7 @@ WORKDIR /src
 COPY . .
 
 RUN go mod tidy
-RUN go build -o cgat ./cmd/cgat
+RUN go build -o caddygat ./cmd/caddygat
 
 FROM alpine:latest
 WORKDIR /
@@ -13,7 +13,7 @@ RUN apk add --no-cache bash
 
 COPY entrypoint.sh .
 
-COPY --from=GOBUILDER /src/cgat /usr/bin/pggat
+COPY --from=GOBUILDER /src/presets /presets
+COPY --from=GOBUILDER /src/caddygat /usr/bin/pggat
 
-ENTRYPOINT ["/entrypoint.sh"]
-cmd ["pggat"]
+CMD ["/entrypoint.sh"]
diff --git a/presets/default.Caddyfile b/presets/default.Caddyfile
new file mode 100644
index 0000000000000000000000000000000000000000..f0e4d67654855b306f43c733fdc465e888e9ac78
--- /dev/null
+++ b/presets/default.Caddyfile
@@ -0,0 +1,3 @@
+:5432 {
+    error "server is not configured"
+}
diff --git a/presets/digitalocean_databases.Caddyfile b/presets/digitalocean_databases.Caddyfile
new file mode 100644
index 0000000000000000000000000000000000000000..813c0ef6bf4aa38c20720ee244930296d9fe0931
--- /dev/null
+++ b/presets/digitalocean_databases.Caddyfile
@@ -0,0 +1,5 @@
+:5432 {
+	ssl
+
+	discovery digitalocean {$PGGAT_DO_API_KEY}
+}
diff --git a/presets/google_cloud_sql.Caddyfile b/presets/google_cloud_sql.Caddyfile
new file mode 100644
index 0000000000000000000000000000000000000000..89bbd21554c809f39f5faaf7c8368e990cd34b4b
--- /dev/null
+++ b/presets/google_cloud_sql.Caddyfile
@@ -0,0 +1,5 @@
+:5432 {
+	ssl
+
+	discovery google_cloud_sql {$PGGAT_GC_PROJECT} {$PGGAT_GC_AUTH_USER:pggat} {$PGGAT_GC_AUTH_PASSWORD}
+}
diff --git a/presets/pgbouncer_spilo.Caddyfile b/presets/pgbouncer_spilo.Caddyfile
new file mode 100644
index 0000000000000000000000000000000000000000..b481eff38e34108537ab61fe698fc049ec950f0f
--- /dev/null
+++ b/presets/pgbouncer_spilo.Caddyfile
@@ -0,0 +1,18 @@
+:{$CONNECTION_POOLER_PORT:5432} {
+    ssl x509_key_pair /etc/ssl/certs/pgbouncer.crt /etc/ssl/certs/pgbouncer.key
+    require_ssl
+
+    pgbouncer_spilo {
+        host {$PGHOST}
+        port {$PGPORT}
+        user {$PGUSER}
+        schema {$PGSCHEMA}
+        password {$PGPASSWORD}
+        mode {$CONNECTION_POOLER_MODE}
+        default_size {$CONNECTION_POOLER_DEFAULT_SIZE}
+        min_size {$CONNECTION_POOLER_MIN_SIZE}
+        reserve_size {$CONNECTION_POOLER_RESERVE_SIZE}
+        max_client_conn {$CONNECTION_POOLER_MAX_CLIENT_CONN}
+        max_db_conn {$CONNECTION_POOLER_MAX_DB_CONN}
+    }
+}
\ No newline at end of file
diff --git a/presets/zalando_kubernetes_operator.Caddyfile b/presets/zalando_kubernetes_operator.Caddyfile
new file mode 100644
index 0000000000000000000000000000000000000000..7e55477200a3850c85ffd53acaa61aa42c945495
--- /dev/null
+++ b/presets/zalando_kubernetes_operator.Caddyfile
@@ -0,0 +1,5 @@
+:5432 {
+	ssl
+
+	discovery zalando_operator {$POSTGRES_OPERATOR_CONFIGURATION_OBJECT}
+}
diff --git a/test/runner.go b/test/runner.go
index ec4094d69ebff927b20d879c8614c873f56c8322..d23e94c631e58064f1d933793dcb7a782834a316 100644
--- a/test/runner.go
+++ b/test/runner.go
@@ -89,8 +89,8 @@ func (T *Runner) prepare(client *gsql.Client, until int) []Capturer {
 	return results
 }
 
-func (T *Runner) runModeL1(dialer recipe.Dialer, client *gsql.Client) error {
-	server, _, err := dialer.Dial()
+func (T *Runner) runModeL1(dialer recipe.Dialer, client *fed.Conn) error {
+	server, err := dialer.Dial()
 	if err != nil {
 		return err
 	}
@@ -127,7 +127,7 @@ func (T *Runner) runModeOnce(dialer recipe.Dialer) ([]Capturer, error) {
 		return nil, err
 	}
 
-	if err := T.runModeL1(dialer, &client); err != nil {
+	if err := T.runModeL1(dialer, fed.NewConn(&client)); err != nil {
 		return nil, err
 	}
 
@@ -142,7 +142,7 @@ func (T *Runner) runModeFail(dialer recipe.Dialer) error {
 			return err
 		}
 
-		if err := T.runModeL1(dialer, &client); err != nil && !errors.Is(err, io.EOF) {
+		if err := T.runModeL1(dialer, fed.NewConn(&client)); err != nil && !errors.Is(err, io.EOF) {
 			return err
 		}
 	}
diff --git a/test/tester_test.go b/test/tester_test.go
index 99e985f37af5a9f681cf693079cda262582903fa..6ef9b43191e68e04c7132e452a46e5202fe78dbd 100644
--- a/test/tester_test.go
+++ b/test/tester_test.go
@@ -2,75 +2,165 @@ package test_test
 
 import (
 	"crypto/rand"
-	"encoding/hex"
+	"encoding/base64"
 	"fmt"
-	"net"
 	_ "net/http/pprof"
 	"strconv"
+	"strings"
 	"testing"
 
-	"gfx.cafe/gfx/pggat/lib/auth"
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig"
+
 	"gfx.cafe/gfx/pggat/lib/auth/credentials"
-	"gfx.cafe/gfx/pggat/lib/bouncer/backends/v0"
-	"gfx.cafe/gfx/pggat/lib/bouncer/frontends/v0"
 	"gfx.cafe/gfx/pggat/lib/gat"
-	"gfx.cafe/gfx/pggat/lib/gat/modules/raw_pools"
+	"gfx.cafe/gfx/pggat/lib/gat/gatcaddyfile"
+	pool_handler "gfx.cafe/gfx/pggat/lib/gat/handlers/pool"
+	"gfx.cafe/gfx/pggat/lib/gat/handlers/rewrite_password"
+	"gfx.cafe/gfx/pggat/lib/gat/matchers"
 	"gfx.cafe/gfx/pggat/lib/gat/pool"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/pools/session"
-	"gfx.cafe/gfx/pggat/lib/gat/pool/pools/transaction"
 	"gfx.cafe/gfx/pggat/lib/gat/pool/recipe"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/session"
+	"gfx.cafe/gfx/pggat/lib/gat/poolers/transaction"
 	"gfx.cafe/gfx/pggat/test"
 	"gfx.cafe/gfx/pggat/test/tests"
 )
 
-func daisyChain(creds auth.Credentials, control recipe.Dialer, n int) (recipe.Dialer, error) {
-	for i := 0; i < n; i++ {
-		var server gat.Server
+type dialer struct {
+	Address  string
+	Username string
+	Password string
+	Database string
+}
+
+var nextPort int
+
+func randAddress() string {
+	nextPort++
+	return "/tmp/.s.PGGAT." + strconv.Itoa(nextPort)
+}
+
+func resolveNetwork(address string) string {
+	if strings.HasPrefix(address, "/") {
+		return "unix"
+	} else {
+		return "tcp"
+	}
+}
+
+func randPassword() (string, error) {
+	var b [20]byte
+	_, err := rand.Read(b[:])
+	if err != nil {
+		return "", err
+	}
+
+	return base64.StdEncoding.EncodeToString(b[:]), nil
+}
+
+func createServer(parent dialer, poolers map[string]caddy.Module) (server gat.ServerConfig, dialers map[string]dialer, err error) {
+	address := randAddress()
+
+	server.Listen = []gat.ListenerConfig{
+		{
+			Address: address,
+		},
+	}
 
-		var options = pool.Options{
-			Credentials: creds,
+	var password string
+	password, err = randPassword()
+	if err != nil {
+		return
+	}
+
+	server.Routes = append(
+		server.Routes,
+		gat.RouteConfig{
+			Handle: gatcaddyfile.JSONModuleObject(
+				&rewrite_password.Module{
+					Password: password,
+				},
+				gatcaddyfile.Handler,
+				"handler",
+				nil,
+			),
+		},
+	)
+
+	for name, pooler := range poolers {
+		p := pool_handler.Module{
+			Config: pool_handler.Config{
+				Pooler: gatcaddyfile.JSONModuleObject(
+					pooler,
+					gatcaddyfile.Pooler,
+					"pooler",
+					nil,
+				),
+
+				ServerAddress: parent.Address,
+
+				ServerUsername: parent.Username,
+				ServerPassword: parent.Password,
+				ServerDatabase: parent.Database,
+			},
 		}
-		if i%2 == 0 {
-			options = transaction.Apply(options)
-		} else {
-			options.ServerResetQuery = "DISCARD ALL"
-			options = session.Apply(options)
+
+		server.Routes = append(server.Routes, gat.RouteConfig{
+			Match: gatcaddyfile.JSONModuleObject(
+				&matchers.Database{
+					Database: name,
+				},
+				gatcaddyfile.Matcher,
+				"matcher",
+				nil,
+			),
+			Handle: gatcaddyfile.JSONModuleObject(
+				&p,
+				gatcaddyfile.Handler,
+				"handler",
+				nil,
+			),
+		})
+
+		if dialers == nil {
+			dialers = make(map[string]dialer)
 		}
+		dialers[name] = dialer{
+			Address:  address,
+			Username: "pooler",
+			Password: password,
+			Database: name,
+		}
+	}
 
-		p := pool.NewPool(options)
-		p.AddRecipe("runner", recipe.NewRecipe(recipe.Options{
-			Dialer: control,
-		}))
+	return
+}
 
-		m, err := raw_pools.NewModule()
-		if err != nil {
-			return recipe.Dialer{}, err
+func daisyChain(config *gat.Config, control dialer, n int) (dialer, error) {
+	for i := 0; i < n; i++ {
+		poolConfig := pool.ManagementConfig{}
+		var pooler caddy.Module
+		if i%2 == 0 {
+			pooler = &transaction.Module{
+				ManagementConfig: poolConfig,
+			}
+		} else {
+			poolConfig.ServerResetQuery = "DISCARD ALL"
+			pooler = &session.Module{
+				ManagementConfig: poolConfig,
+			}
 		}
-		m.Add("runner", "pool", p)
-		server.AddModule(m)
 
-		listener, err := server.Listen("tcp", ":0")
+		server, dialers, err := createServer(control, map[string]caddy.Module{
+			"pool": pooler,
+		})
+
 		if err != nil {
-			return recipe.Dialer{}, err
+			return dialer{}, err
 		}
-		port := listener.Addr().(*net.TCPAddr).Port
 
-		go func() {
-			err := server.Serve(listener, frontends.AcceptOptions{})
-			if err != nil {
-				panic(err)
-			}
-		}()
-
-		control = recipe.Dialer{
-			Network: "tcp",
-			Address: ":" + strconv.Itoa(port),
-			AcceptOptions: backends.AcceptOptions{
-				Username:    "runner",
-				Credentials: creds,
-				Database:    "pool",
-			},
-		}
+		control = dialers["pool"]
+		config.Servers = append(config.Servers, server)
 	}
 
 	return control, nil
@@ -78,96 +168,80 @@ func daisyChain(creds auth.Credentials, control recipe.Dialer, n int) (recipe.Di
 
 func TestTester(t *testing.T) {
 	control := recipe.Dialer{
-		Network: "tcp",
-		Address: "localhost:5432",
-		AcceptOptions: backends.AcceptOptions{
+		Network:  "tcp",
+		Address:  "localhost:5432",
+		Username: "postgres",
+		Credentials: credentials.Cleartext{
 			Username: "postgres",
-			Credentials: credentials.Cleartext{
-				Username: "postgres",
-				Password: "password",
-			},
-			Database: "postgres",
+			Password: "password",
 		},
+		Database: "postgres",
 	}
 
-	// generate random password for testing
-	var raw [32]byte
-	_, err := rand.Read(raw[:])
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	password := hex.EncodeToString(raw[:])
-	creds := credentials.Cleartext{
-		Username: "runner",
-		Password: password,
-	}
+	config := gat.Config{}
 
-	parent, err := daisyChain(creds, control, 16)
+	parent, err := daisyChain(&config, dialer{
+		Address:  "localhost:5432",
+		Username: "postgres",
+		Password: "password",
+		Database: "postgres",
+	}, 16)
 	if err != nil {
 		t.Error(err)
 		return
 	}
 
-	var server gat.Server
-
-	m, err := raw_pools.NewModule()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	transactionPool := pool.NewPool(transaction.Apply(pool.Options{
-		Credentials: creds,
-	}))
-	transactionPool.AddRecipe("runner", recipe.NewRecipe(recipe.Options{
-		Dialer: parent,
-	}))
-	m.Add("runner", "transaction", transactionPool)
-
-	sessionPool := pool.NewPool(session.Apply(pool.Options{
-		Credentials:      creds,
-		ServerResetQuery: "discard all",
-	}))
-	sessionPool.AddRecipe("runner", recipe.NewRecipe(recipe.Options{
-		Dialer: parent,
-	}))
-	m.Add("runner", "session", sessionPool)
-
-	server.AddModule(m)
-
-	listener, err := server.Listen("tcp", ":0")
+	server, dialers, err := createServer(parent, map[string]caddy.Module{
+		"transaction": &transaction.Module{},
+		"session": &session.Module{
+			ManagementConfig: pool.ManagementConfig{
+				ServerResetQuery: "discard all",
+			},
+		},
+	})
 	if err != nil {
 		t.Error(err)
 		return
 	}
-	port := listener.Addr().(*net.TCPAddr).Port
 
-	go func() {
-		err := server.Serve(listener, frontends.AcceptOptions{})
-		if err != nil {
-			t.Error(err)
-		}
-	}()
+	config.Servers = append(config.Servers, server)
 
 	transactionDialer := recipe.Dialer{
-		Network: "tcp",
-		Address: ":" + strconv.Itoa(port),
-		AcceptOptions: backends.AcceptOptions{
-			Username:    "runner",
-			Credentials: creds,
-			Database:    "transaction",
-		},
+		Network:  resolveNetwork(dialers["transaction"].Address),
+		Address:  dialers["transaction"].Address,
+		Username: dialers["transaction"].Username,
+		Credentials: credentials.FromString(
+			dialers["transaction"].Username,
+			dialers["transaction"].Password,
+		),
+		Database: "transaction",
 	}
 	sessionDialer := recipe.Dialer{
-		Network: "tcp",
-		Address: ":" + strconv.Itoa(port),
-		AcceptOptions: backends.AcceptOptions{
-			Username:    "runner",
-			Credentials: creds,
-			Database:    "session",
+		Network:  resolveNetwork(dialers["transaction"].Address),
+		Address:  dialers["session"].Address,
+		Username: dialers["session"].Username,
+		Credentials: credentials.FromString(
+			dialers["session"].Username,
+			dialers["session"].Password,
+		),
+		Database: "session",
+	}
+
+	caddyConfig := caddy.Config{
+		AppsRaw: caddy.ModuleMap{
+			"pggat": caddyconfig.JSON(config, nil),
 		},
 	}
 
+	if err = caddy.Run(&caddyConfig); err != nil {
+		t.Error(err)
+		return
+	}
+
+	defer func() {
+		_ = caddy.Stop()
+	}()
+
 	tester := test.NewTester(test.Config{
 		Stress: 8,