diff --git a/lib/gat/gatcaddyfile/discoverer.go b/lib/gat/gatcaddyfile/discoverer.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec4fb4225c09bbcf9f713a126ed16ea45b8bcfef
--- /dev/null
+++ b/lib/gat/gatcaddyfile/discoverer.go
@@ -0,0 +1,25 @@
+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"
+)
+
+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
+	})
+}
diff --git a/lib/gat/gatcaddyfile/gattype.go b/lib/gat/gatcaddyfile/gattype.go
index 1114f41870316cd997bdec921c3fe6137f4a9599..db839f49f00a91d507e95e354b5af433787c95da 100644
--- a/lib/gat/gatcaddyfile/gattype.go
+++ b/lib/gat/gatcaddyfile/gattype.go
@@ -2,7 +2,6 @@ package gatcaddyfile
 
 import (
 	"encoding/json"
-	"log"
 	"strings"
 
 	"github.com/caddyserver/caddy/v2"
@@ -66,15 +65,10 @@ func (ServerType) Setup(blocks []caddyfile.ServerBlock, m map[string]any) (*cadd
 						&warnings,
 					)
 				} else {
-					unmarshaller, ok := sslServers[d.Val()]
-					if !ok {
-						return nil, nil, d.Errf(`unknown ssl server "%s"`, d.Val())
-					}
-
 					var err error
-					val, err = unmarshaller.JSONModuleObject(
+					val, err = UnmarshalDirectiveJSONModuleObject(
 						d,
-						"pggat.ssl.servers",
+						SSLServer,
 						"provider",
 						&warnings,
 					)
@@ -105,26 +99,57 @@ func (ServerType) Setup(blocks []caddyfile.ServerBlock, m map[string]any) (*cadd
 				// 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
 						}
 
-						log.Println(d.Val())
-						for d.NextArg() {
-							log.Println(d.Val())
+						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 {
@@ -136,7 +161,7 @@ func (ServerType) Setup(blocks []caddyfile.ServerBlock, m map[string]any) (*cadd
 				}
 				namedMatchers[name] = matcher
 			default:
-				unmarshaller, ok := handlers[d.Val()]
+				unmarshaller, ok := LookupDirective(Handler, d.Val())
 				if !ok {
 					return nil, nil, d.Errf(`unknown handler "%s"`, d.Val())
 				}
@@ -169,7 +194,7 @@ func (ServerType) Setup(blocks []caddyfile.ServerBlock, m map[string]any) (*cadd
 				var err error
 				route.Handle, err = unmarshaller.JSONModuleObject(
 					d,
-					"pggat.handlers",
+					Handler,
 					"handler",
 					&warnings,
 				)
diff --git a/lib/gat/gatcaddyfile/handler.go b/lib/gat/gatcaddyfile/handler.go
index c3f628bbfc8c3bca4b8d7e1f21ed971c048f82b4..06504036fe43a33a7ced31c40ee11e9de8fd04d1 100644
--- a/lib/gat/gatcaddyfile/handler.go
+++ b/lib/gat/gatcaddyfile/handler.go
@@ -1,31 +1,32 @@
 package gatcaddyfile
 
 import (
-	"fmt"
+	"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/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"
 )
 
-var handlers map[string]Unmarshaller
-
-func RegisterHandlerDirective(directive string, unmarshaller Unmarshaller) {
-	if _, ok := handlers[directive]; ok {
-		panic(fmt.Sprintf(`duplicate handler directive "%s"`, directive))
-	}
-	if handlers == nil {
-		handlers = make(map[string]Unmarshaller)
-	}
-	handlers[directive] = unmarshaller
-}
-
 func init() {
-	RegisterHandlerDirective("require_ssl", func(d *caddyfile.Dispenser) (caddy.Module, error) {
+	RegisterDirective(Handler, "require_ssl", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
 		var ssl = true
-		if d.Next() {
+		if d.NextArg() {
 			switch d.Val() {
 			case "true":
 				ssl = true
@@ -39,13 +40,209 @@ func init() {
 			SSL: ssl,
 		}, nil
 	})
-	RegisterHandlerDirective("pgbouncer", func(d *caddyfile.Dispenser) (caddy.Module, error) {
+	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, "pgbouncer", func(d *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
 		var config = "pgbouncer.ini"
-		if d.Next() {
+		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.Pool{
+						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
+				}
+
+				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..fe5ac9b86e1b83fe78bd3db58fdaecf83330cee5
--- /dev/null
+++ b/lib/gat/gatcaddyfile/pooler.go
@@ -0,0 +1,55 @@
+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 init() {
+	RegisterDirective(Pooler, "transaction", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := transaction.Pool{
+			ManagementConfig: defaultPoolManagementConfig,
+		}
+
+		if !d.NextBlock(d.Nesting()) {
+			return &module, nil
+		}
+
+		// TODO(garet)
+		panic("TODO(garet)")
+	})
+	RegisterDirective(Pooler, "session", func(d *caddyfile.Dispenser, warnings *[]caddyconfig.Warning) (caddy.Module, error) {
+		module := session.Pool{
+			ManagementConfig: defaultPoolManagementConfig,
+		}
+
+		if !d.NextBlock(d.Nesting()) {
+			return &module, nil
+		}
+
+		// TODO(garet)
+		panic("TODO(garet)")
+	})
+}
diff --git a/lib/gat/gatcaddyfile/ssl.go b/lib/gat/gatcaddyfile/ssl.go
index 01f35ffa3a378b1b3a20725ab0f8124316bdb19f..798d6bc95f2b5c948055b2e73b9eb3081f5c1d1f 100644
--- a/lib/gat/gatcaddyfile/ssl.go
+++ b/lib/gat/gatcaddyfile/ssl.go
@@ -1,28 +1,20 @@
 package gatcaddyfile
 
 import (
-	"fmt"
-
 	"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"
 )
 
-var sslServers map[string]Unmarshaller
-
-func RegisterSSLServerDirective(directive string, unmarshaller Unmarshaller) {
-	if _, ok := sslServers[directive]; ok {
-		panic(fmt.Sprintf(`duplicate ssl server directive "%s"`, directive))
-	}
-	if sslServers == nil {
-		sslServers = make(map[string]Unmarshaller)
-	}
-	sslServers[directive] = unmarshaller
-}
-
 func init() {
-	RegisterSSLServerDirective("self_signed", func(_ *caddyfile.Dispenser) (caddy.Module, error) {
+	RegisterDirective(SSLServer, "self_signed", func(_ *caddyfile.Dispenser, _ *[]caddyconfig.Warning) (caddy.Module, error) {
 		return &self_signed.Server{}, 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
index c5edbcd83f076aad137277ad4944cb0d19046676..e75b67241eabbb60cd0afda77097bcc87492e540 100644
--- a/lib/gat/gatcaddyfile/unmarshaller.go
+++ b/lib/gat/gatcaddyfile/unmarshaller.go
@@ -10,7 +10,7 @@ import (
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 )
 
-type Unmarshaller func(*caddyfile.Dispenser) (caddy.Module, error)
+type Unmarshaller func(*caddyfile.Dispenser, *[]caddyconfig.Warning) (caddy.Module, error)
 
 func (T Unmarshaller) JSONModuleObject(
 	d *caddyfile.Dispenser,
@@ -18,16 +18,35 @@ func (T Unmarshaller) JSONModuleObject(
 	inlineKey string,
 	warnings *[]caddyconfig.Warning,
 ) (json.RawMessage, error) {
-	module, err := T(d)
+	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 {
-		return nil, fmt.Errorf(`expected item in namespace "%s" but got "%s"`, namespace, rawModuleID)
+		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(
@@ -35,5 +54,5 @@ func (T Unmarshaller) JSONModuleObject(
 		inlineKey,
 		moduleID,
 		warnings,
-	), nil
+	)
 }
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/handlers/discovery/config.go b/lib/gat/handlers/discovery/config.go
index 2d9c1581c7994c3d4801e3c8b256fed919b9ed42..d21a5533ae395c025a5ebe4278e81ab84e20ad2b 100644
--- a/lib/gat/handlers/discovery/config.go
+++ b/lib/gat/handlers/discovery/config.go
@@ -18,5 +18,5 @@ type Config struct {
 	ServerSSLMode bouncer.SSLMode `json:"server_ssl_mode"`
 	ServerSSL     json.RawMessage `json:"server_ssl" caddy:"namespace=pggat.ssl.clients inline_key=provider"`
 
-	ServerStartupParameters map[string]string `json:"server_startup_parameters"`
+	ServerStartupParameters map[string]string `json:"server_startup_parameters,omitempty"`
 }
diff --git a/lib/gat/handlers/rewrite_parameters/module.go b/lib/gat/handlers/rewrite_parameter/module.go
similarity index 53%
rename from lib/gat/handlers/rewrite_parameters/module.go
rename to lib/gat/handlers/rewrite_parameter/module.go
index 44a988179bd80af82e666f8087cfdb0c2d4ea2fc..89b056fb57c4ef8248bcbcfc454e17270dc207aa 100644
--- a/lib/gat/handlers/rewrite_parameters/module.go
+++ b/lib/gat/handlers/rewrite_parameter/module.go
@@ -1,4 +1,4 @@
-package rewrite_parameters
+package rewrite_parameter
 
 import (
 	"github.com/caddyserver/caddy/v2"
@@ -13,42 +13,27 @@ func init() {
 }
 
 type Module struct {
-	Parameters map[string]string `json:"parameters"`
-
-	parameters map[strutil.CIString]string
+	Key   strutil.CIString `json:"key"`
+	Value string           `json:"value"`
 }
 
 func (T *Module) CaddyModule() caddy.ModuleInfo {
 	return caddy.ModuleInfo{
-		ID: "pggat.handlers.rewrite_parameters",
+		ID: "pggat.handlers.rewrite_parameter",
 		New: func() caddy.Module {
 			return new(Module)
 		},
 	}
 }
 
-func (T *Module) Provision(ctx caddy.Context) error {
-	T.parameters = make(map[strutil.CIString]string, len(T.Parameters))
-
-	for key, value := range T.Parameters {
-		T.parameters[strutil.MakeCIString(key)] = value
-	}
-
-	return nil
-}
-
 func (T *Module) Handle(conn *fed.Conn) error {
 	if conn.InitialParameters == nil {
 		conn.InitialParameters = make(map[strutil.CIString]string)
 	}
-
-	for key, value := range T.parameters {
-		conn.InitialParameters[key] = value
-	}
+	conn.InitialParameters[T.Key] = T.Value
 
 	return nil
 }
 
 var _ gat.Handler = (*Module)(nil)
 var _ caddy.Module = (*Module)(nil)
-var _ caddy.Provisioner = (*Module)(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/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/startupparameters.go b/lib/gat/matchers/startupparameters.go
deleted file mode 100644
index 975e4d6e7d27248d7c8f42fc31cf62578ce01cfe..0000000000000000000000000000000000000000
--- a/lib/gat/matchers/startupparameters.go
+++ /dev/null
@@ -1,50 +0,0 @@
-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((*StartupParameters)(nil))
-}
-
-type StartupParameters struct {
-	Parameters map[string]string `json:"startup_parameters"`
-
-	parameters map[strutil.CIString]string
-}
-
-func (T *StartupParameters) CaddyModule() caddy.ModuleInfo {
-	return caddy.ModuleInfo{
-		ID: "pggat.matchers.startup_parameters",
-		New: func() caddy.Module {
-			return new(StartupParameters)
-		},
-	}
-}
-
-func (T *StartupParameters) Provision(ctx caddy.Context) error {
-	T.parameters = make(map[strutil.CIString]string, len(T.Parameters))
-	for key, value := range T.Parameters {
-		T.parameters[strutil.MakeCIString(key)] = value
-	}
-
-	return nil
-}
-
-func (T *StartupParameters) Matches(conn *fed.Conn) bool {
-	for key, value := range T.parameters {
-		if conn.InitialParameters[key] != value {
-			return false
-		}
-	}
-	return true
-}
-
-var _ gat.Matcher = (*StartupParameters)(nil)
-var _ caddy.Module = (*StartupParameters)(nil)
-var _ caddy.Provisioner = (*StartupParameters)(nil)
diff --git a/lib/gat/standard/standard.go b/lib/gat/standard/standard.go
index da50e5d3733f7971e8d74355f175b52ce4f6a389..bbe9dfe73b22fd7d956113e782c164779c3a4285 100644
--- a/lib/gat/standard/standard.go
+++ b/lib/gat/standard/standard.go
@@ -17,7 +17,7 @@ import (
 	_ "gfx.cafe/gfx/pggat/lib/gat/handlers/allowed_startup_parameters"
 	_ "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_parameters"
+	_ "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"