Repository: matrix-org/dendrite
Pull request #3011 (2023-03-14 03:22:14)

Update docs and sample config for the relay_api
This adds an empty `relay_api` section to the sample configuration. For SQLite environments, or others where a `database.connection_string` is needed for each section, there should be an entry in the configuration sample as a basis.

This PR also changes the "Configuring Dendrite" documentation in that respect.

The requirement was introduced in #2917. When upgrading dendrite, it will complain about `relay_api.database.connection_string` not being configured.

### Pull Request Checklist

<!-- Please read https://matrix-org.github.io/dendrite/development/contributing before submitting your pull request -->

* [x] I have added Go unit tests or [Complement integration tests](https://github.com/matrix-org/complement) for this PR _or_ I have justified why this PR doesn't need tests
* [x] Pull request includes a [sign off below using a legally identifiable name](https://matrix-org.github.io/dendrite/development/contributing#sign-off) _or_ I have already signed off privately

Signed-off-by: `Alex Kirk <akirk@users.noreply.github.com>`



Patch:
diff --git a/.github/workflows/schedules.yaml b/.github/workflows/schedules.yaml
index dff9b34c..25459491 100644
--- a/.github/workflows/schedules.yaml
+++ b/.github/workflows/schedules.yaml
@@ -65,10 +65,11 @@ jobs:
         uses: actions/upload-artifact@v2
         if: ${{ always() }}
         with:
-          name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
+          name: Sytest Logs - ${{ job.status }} - (Dendrite ${{ join(matrix.*, ' ') }})
           path: |
             /logs/results.tap
             /logs/**/*.log*
+            /logs/**/covdatafiles/**
 
   sytest-coverage:
     timeout-minutes: 5
@@ -85,16 +86,15 @@ jobs:
           cache: true
       - name: Download all artifacts
         uses: actions/download-artifact@v3
-      - name: Install gocovmerge
-        run: go install github.com/wadey/gocovmerge@latest
-      - name: Run gocovmerge
+      - name: Collect coverage
         run: |
-          find -name 'integrationcover.log' -printf '"%p"\n' | xargs gocovmerge | grep -Ev 'relayapi|setup/mscs|api_trace' > sytest.cov
-          go tool cover -func=sytest.cov
+          go tool covdata textfmt -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o sytest.cov
+          grep -Ev 'relayapi|setup/mscs|api_trace' sytest.cov > final.cov
+          go tool covdata func -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
         with:
-          files: ./sytest.cov
+          files: ./final.cov
           flags: sytest
           fail_ci_if_error: true
 
@@ -167,7 +167,7 @@ jobs:
           cat <<EOF > /tmp/posttest.sh
           #!/bin/bash
           mkdir -p /tmp/Complement/logs/\$2/\$1/
-          docker cp \$1:/dendrite/complementcover.log /tmp/Complement/logs/\$2/\$1/
+          docker cp \$1:/tmp/covdatafiles/. /tmp/Complement/logs/\$2/\$1/
           EOF
           
           chmod +x /tmp/posttest.sh
@@ -188,9 +188,9 @@ jobs:
         uses: actions/upload-artifact@v2
         if: ${{ always() }}
         with:
-          name: Complement Logs - (Dendrite, ${{ join(matrix.*, ', ') }})
+          name: Complement Logs - (Dendrite ${{ join(matrix.*, ' ') }})
           path: |
-            /tmp/Complement/**/complementcover.log
+            /tmp/Complement/logs/**
 
   complement-coverage:
     timeout-minutes: 5
@@ -207,16 +207,15 @@ jobs:
           cache: true
       - name: Download all artifacts
         uses: actions/download-artifact@v3
-      - name: Install gocovmerge
-        run: go install github.com/wadey/gocovmerge@latest
-      - name: Run gocovmerge
+      - name: Collect coverage
         run: |
-          find -name 'complementcover.log' -printf '"%p"\n' | xargs gocovmerge | grep -Ev 'relayapi|setup/mscs|api_trace' > complement.cov
-          go tool cover -func=complement.cov
+          go tool covdata textfmt -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o complement.cov
+          grep -Ev 'relayapi|setup/mscs|api_trace' complement.cov > final.cov
+          go tool covdata func -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v3
         with:
-          files: ./complement.cov
+          files: ./final.cov
           flags: complement
           fail_ci_if_error: true
 
diff --git a/appservice/appservice.go b/appservice/appservice.go
index 5b1b93de..d13d9eb1 100644
--- a/appservice/appservice.go
+++ b/appservice/appservice.go
@@ -21,6 +21,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/sirupsen/logrus"
 
 	"github.com/matrix-org/gomatrixserverlib"
@@ -29,7 +31,6 @@ import (
 	"github.com/matrix-org/dendrite/appservice/consumers"
 	"github.com/matrix-org/dendrite/appservice/query"
 	roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 )
@@ -37,7 +38,9 @@ import (
 // NewInternalAPI returns a concerete implementation of the internal API. Callers
 // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
 func NewInternalAPI(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	cfg *config.Dendrite,
+	natsInstance *jetstream.NATSInstance,
 	userAPI userapi.AppserviceUserAPI,
 	rsAPI roomserverAPI.RoomserverInternalAPI,
 ) appserviceAPI.AppServiceInternalAPI {
@@ -46,7 +49,7 @@ func NewInternalAPI(
 		Transport: &http.Transport{
 			DisableKeepAlives: true,
 			TLSClientConfig: &tls.Config{
-				InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation,
+				InsecureSkipVerify: cfg.AppServiceAPI.DisableTLSValidation,
 			},
 			Proxy: http.ProxyFromEnvironment,
 		},
@@ -55,21 +58,21 @@ func NewInternalAPI(
 	// outbound and inbound requests (inbound only for the internal API)
 	appserviceQueryAPI := &query.AppServiceQueryAPI{
 		HTTPClient:    client,
-		Cfg:           &base.Cfg.AppServiceAPI,
+		Cfg:           &cfg.AppServiceAPI,
 		ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
 		CacheMu:       sync.Mutex{},
 	}
 
-	if len(base.Cfg.Derived.ApplicationServices) == 0 {
+	if len(cfg.Derived.ApplicationServices) == 0 {
 		return appserviceQueryAPI
 	}
 
 	// Wrap application services in a type that relates the application service and
 	// a sync.Cond object that can be used to notify workers when there are new
 	// events to be sent out.
-	for _, appservice := range base.Cfg.Derived.ApplicationServices {
+	for _, appservice := range cfg.Derived.ApplicationServices {
 		// Create bot account for this AS if it doesn't already exist
-		if err := generateAppServiceAccount(userAPI, appservice, base.Cfg.Global.ServerName); err != nil {
+		if err := generateAppServiceAccount(userAPI, appservice, cfg.Global.ServerName); err != nil {
 			logrus.WithFields(logrus.Fields{
 				"appservice": appservice.ID,
 			}).WithError(err).Panicf("failed to generate bot account for appservice")
@@ -78,9 +81,9 @@ func NewInternalAPI(
 
 	// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
 	// We can't add ASes at runtime so this is safe to do.
-	js, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
+	js, _ := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
 	consumer := consumers.NewOutputRoomEventConsumer(
-		base.ProcessContext, &base.Cfg.AppServiceAPI,
+		processContext, &cfg.AppServiceAPI,
 		client, js, rsAPI,
 	)
 	if err := consumer.Start(); err != nil {
diff --git a/build/dendritejs-pinecone/main.go b/build/dendritejs-pinecone/main.go
index 44e52286..bc9535fc 100644
--- a/build/dendritejs-pinecone/main.go
+++ b/build/dendritejs-pinecone/main.go
@@ -29,11 +29,14 @@ import (
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
 	"github.com/matrix-org/dendrite/federationapi"
+	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/setup"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/userapi"
 
 	"github.com/matrix-org/gomatrixserverlib"
@@ -157,9 +160,8 @@ func startup() {
 	pManager.AddPeer("wss://pinecone.matrix.org/public")
 
 	cfg := &config.Dendrite{}
-	cfg.Defaults(true)
+	cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: false})
 	cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
-	cfg.AppServiceAPI.Database.ConnectionString = "file:/idb/dendritejs_appservice.db"
 	cfg.FederationAPI.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
 	cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
 	cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
@@ -176,28 +178,30 @@ func startup() {
 	if err := cfg.Derive(); err != nil {
 		logrus.Fatalf("Failed to derive values from config: %s", err)
 	}
-	base := base.NewBaseDendrite(cfg)
-	defer base.Close() // nolint: errcheck
+	natsInstance := jetstream.NATSInstance{}
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
+	caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
+	rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
 
-	rsAPI := roomserver.NewInternalAPI(base)
-
-	federation := conn.CreateFederationClient(base, pSessions)
+	federation := conn.CreateFederationClient(cfg, pSessions)
 
 	serverKeyAPI := &signing.YggdrasilKeys{}
 	keyRing := serverKeyAPI.KeyRing()
 
-	userAPI := userapi.NewInternalAPI(base, rsAPI, federation)
+	userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation)
 
 	asQuery := appservice.NewInternalAPI(
-		base, userAPI, rsAPI,
+		processCtx, cfg, &natsInstance, userAPI, rsAPI,
 	)
 	rsAPI.SetAppserviceAPI(asQuery)
-	fedSenderAPI := federationapi.NewInternalAPI(base, federation, rsAPI, base.Caches, keyRing, true)
+	fedSenderAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true)
 	rsAPI.SetFederationAPI(fedSenderAPI, keyRing)
 
 	monolith := setup.Monolith{
-		Config:    base.Cfg,
-		Client:    conn.CreateClient(base, pSessions),
+		Config:    cfg,
+		Client:    conn.CreateClient(pSessions),
 		FedClient: federation,
 		KeyRing:   keyRing,
 
@@ -208,15 +212,15 @@ func startup() {
 		//ServerKeyAPI:        serverKeyAPI,
 		ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation),
 	}
-	monolith.AddAllPublicRoutes(base)
+	monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
 
 	httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
-	httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
-	httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
+	httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
+	httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
 
 	p2pRouter := pSessions.Protocol("matrix").HTTP().Mux()
-	p2pRouter.Handle(httputil.PublicFederationPathPrefix, base.PublicFederationAPIMux)
-	p2pRouter.Handle(httputil.PublicMediaPathPrefix, base.PublicMediaAPIMux)
+	p2pRouter.Handle(httputil.PublicFederationPathPrefix, routers.Federation)
+	p2pRouter.Handle(httputil.PublicMediaPathPrefix, routers.Media)
 
 	// Expose the matrix APIs via fetch - for local traffic
 	go func() {
diff --git a/build/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go
index 16797eec..2e2ca04d 100644
--- a/build/gobind-pinecone/monolith.go
+++ b/build/gobind-pinecone/monolith.go
@@ -30,6 +30,9 @@ import (
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/relay"
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
 	"github.com/matrix-org/dendrite/federationapi/api"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/process"
 	userapiAPI "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/pinecone/types"
@@ -187,7 +190,7 @@ func (m *DendriteMonolith) SetRelayServers(nodeID string, uris string) {
 		relay.UpdateNodeRelayServers(
 			gomatrixserverlib.ServerName(nodeKey),
 			relays,
-			m.p2pMonolith.BaseDendrite.Context(),
+			m.p2pMonolith.ProcessCtx.Context(),
 			m.p2pMonolith.GetFederationAPI(),
 		)
 	}
@@ -214,7 +217,7 @@ func (m *DendriteMonolith) GetRelayServers(nodeID string) string {
 	} else {
 		request := api.P2PQueryRelayServersRequest{Server: gomatrixserverlib.ServerName(nodeKey)}
 		response := api.P2PQueryRelayServersResponse{}
-		err := m.p2pMonolith.GetFederationAPI().P2PQueryRelayServers(m.p2pMonolith.BaseDendrite.Context(), &request, &response)
+		err := m.p2pMonolith.GetFederationAPI().P2PQueryRelayServers(m.p2pMonolith.ProcessCtx.Context(), &request, &response)
 		if err != nil {
 			logrus.Warnf("Failed obtaining list of this node's relay servers: %s", err.Error())
 			return ""
@@ -346,10 +349,14 @@ func (m *DendriteMonolith) Start() {
 	// This isn't actually fixed: https://github.com/blevesearch/zapx/pull/147
 	cfg.SyncAPI.Fulltext.Enabled = false
 
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
+
 	enableRelaying := false
 	enableMetrics := false
 	enableWebsockets := false
-	m.p2pMonolith.SetupDendrite(cfg, 65432, enableRelaying, enableMetrics, enableWebsockets)
+	m.p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, 65432, enableRelaying, enableMetrics, enableWebsockets)
 	m.p2pMonolith.StartMonolith()
 }
 
diff --git a/build/gobind-yggdrasil/monolith.go b/build/gobind-yggdrasil/monolith.go
index 32af611a..7ce1892c 100644
--- a/build/gobind-yggdrasil/monolith.go
+++ b/build/gobind-yggdrasil/monolith.go
@@ -12,6 +12,7 @@ import (
 	"path/filepath"
 	"time"
 
+	"github.com/getsentry/sentry-go"
 	"github.com/gorilla/mux"
 	"github.com/matrix-org/dendrite/appservice"
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
@@ -19,11 +20,15 @@ import (
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
 	"github.com/matrix-org/dendrite/federationapi"
 	"github.com/matrix-org/dendrite/federationapi/api"
+	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/setup"
-	"github.com/matrix-org/dendrite/setup/base"
+	basepkg "github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/userapi"
@@ -148,25 +153,71 @@ func (m *DendriteMonolith) Start() {
 		panic(err)
 	}
 
-	base := base.NewBaseDendrite(cfg)
-	base.ConfigureAdminEndpoints()
-	m.processContext = base.ProcessContext
-	defer base.Close() // nolint: errcheck
+	configErrors := &config.ConfigErrors{}
+	cfg.Verify(configErrors)
+	if len(*configErrors) > 0 {
+		for _, err := range *configErrors {
+			logrus.Errorf("Configuration error: %s", err)
+		}
+		logrus.Fatalf("Failed to start due to configuration errors")
+	}
+
+	internal.SetupStdLogging()
+	internal.SetupHookLogging(cfg.Logging)
+	internal.SetupPprof()
+
+	logrus.Infof("Dendrite version %s", internal.VersionString())
+
+	if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
+		logrus.Warn("Open registration is enabled")
+	}
 
-	federation := ygg.CreateFederationClient(base)
+	closer, err := cfg.SetupTracing()
+	if err != nil {
+		logrus.WithError(err).Panicf("failed to start opentracing")
+	}
+	defer closer.Close()
+
+	if cfg.Global.Sentry.Enabled {
+		logrus.Info("Setting up Sentry for debugging...")
+		err = sentry.Init(sentry.ClientOptions{
+			Dsn:              cfg.Global.Sentry.DSN,
+			Environment:      cfg.Global.Sentry.Environment,
+			Debug:            true,
+			ServerName:       string(cfg.Global.ServerName),
+			Release:          "dendrite@" + internal.VersionString(),
+			AttachStacktrace: true,
+		})
+		if err != nil {
+			logrus.WithError(err).Panic("failed to start Sentry")
+		}
+	}
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
+	basepkg.ConfigureAdminEndpoints(processCtx, routers)
+	m.processContext = processCtx
+	defer func() {
+		processCtx.ShutdownDendrite()
+		processCtx.WaitForShutdown()
+	}() // nolint: errcheck
+
+	federation := ygg.CreateFederationClient(cfg)
 
 	serverKeyAPI := &signing.YggdrasilKeys{}
 	keyRing := serverKeyAPI.KeyRing()
 
-	rsAPI := roomserver.NewInternalAPI(base)
+	caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
+	natsInstance := jetstream.NATSInstance{}
+	rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
 
 	fsAPI := federationapi.NewInternalAPI(
-		base, federation, rsAPI, base.Caches, keyRing, true,
+		processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
 	)
 
-	userAPI := userapi.NewInternalAPI(base, rsAPI, federation)
+	userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation)
 
-	asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
+	asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
 	rsAPI.SetAppserviceAPI(asAPI)
 
 	// The underlying roomserver implementation needs to be able to call the fedsender.
@@ -174,8 +225,8 @@ func (m *DendriteMonolith) Start() {
 	rsAPI.SetFederationAPI(fsAPI, keyRing)
 
 	monolith := setup.Monolith{
-		Config:    base.Cfg,
-		Client:    ygg.CreateClient(base),
+		Config:    cfg,
+		Client:    ygg.CreateClient(),
 		FedClient: federation,
 		KeyRing:   keyRing,
 
@@ -187,17 +238,17 @@ func (m *DendriteMonolith) Start() {
 			ygg, fsAPI, federation,
 		),
 	}
-	monolith.AddAllPublicRoutes(base)
+	monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
 
 	httpRouter := mux.NewRouter()
-	httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
-	httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
-	httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(base.DendriteAdminMux)
-	httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(base.SynapseAdminMux)
+	httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
+	httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
+	httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
+	httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
 
 	yggRouter := mux.NewRouter()
-	yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
-	yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
+	yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
+	yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
 
 	// Build both ends of a HTTP multiplex.
 	m.httpServer = &http.Server{
diff --git a/build/scripts/Complement.Dockerfile b/build/scripts/Complement.Dockerfile
index 70bbe8f9..453d8976 100644
--- a/build/scripts/Complement.Dockerfile
+++ b/build/scripts/Complement.Dockerfile
@@ -1,6 +1,6 @@
 #syntax=docker/dockerfile:1.2
 
-FROM golang:1.18-stretch as build
+FROM golang:1.20-bullseye as build
 RUN apt-get update && apt-get install -y sqlite3
 WORKDIR /build
 
@@ -17,7 +17,7 @@ RUN --mount=target=. \
     CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
     CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
     CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
-    CGO_ENABLED=${CGO} go test -c -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
+    CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
     cp build/scripts/complement-cmd.sh /complement-cmd.sh
 
 WORKDIR /dendrite
diff --git a/build/scripts/ComplementPostgres.Dockerfile b/build/scripts/ComplementPostgres.Dockerfile
index d4b6d3f7..77071b45 100644
--- a/build/scripts/ComplementPostgres.Dockerfile
+++ b/build/scripts/ComplementPostgres.Dockerfile
@@ -1,19 +1,19 @@
 #syntax=docker/dockerfile:1.2
 
-FROM golang:1.18-stretch as build
+FROM golang:1.20-bullseye as build
 RUN apt-get update && apt-get install -y postgresql
 WORKDIR /build
 
 # No password when connecting over localhost
-RUN sed -i "s%127.0.0.1/32            md5%127.0.0.1/32            trust%g" /etc/postgresql/9.6/main/pg_hba.conf && \
+RUN sed -i "s%127.0.0.1/32            md5%127.0.0.1/32            trust%g" /etc/postgresql/13/main/pg_hba.conf && \
     # Bump up max conns for moar concurrency
-    sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/9.6/main/postgresql.conf
+    sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/13/main/postgresql.conf
 
 # This entry script starts postgres, waits for it to be up then starts dendrite
 RUN echo '\
     #!/bin/bash -eu \n\
     pg_lsclusters \n\
-    pg_ctlcluster 9.6 main start \n\
+    pg_ctlcluster 13 main start \n\
     \n\
     until pg_isready \n\
     do \n\
@@ -35,7 +35,7 @@ RUN --mount=target=. \
     CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
     CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
     CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
-    CGO_ENABLED=${CGO} go test -c -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
+    CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
     cp build/scripts/complement-cmd.sh /complement-cmd.sh
 
 WORKDIR /dendrite
diff --git a/build/scripts/complement-cmd.sh b/build/scripts/complement-cmd.sh
index 52b063d0..3f6ed070 100755
--- a/build/scripts/complement-cmd.sh
+++ b/build/scripts/complement-cmd.sh
@@ -2,14 +2,15 @@
 
 # This script is intended to be used inside a docker container for Complement
 
+export GOCOVERDIR=/tmp/covdatafiles
+mkdir -p "${GOCOVERDIR}"
 if [[ "${COVER}" -eq 1 ]]; then
   echo "Running with coverage"
   exec /dendrite/dendrite-cover \
     --really-enable-open-registration \
     --tls-cert server.crt \
     --tls-key server.key \
-    --config dendrite.yaml \
-    --test.coverprofile=complementcover.log
+    --config dendrite.yaml
 else
   echo "Not running with coverage"
   exec /dendrite/dendrite \
diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go
index e9985d43..d35f1d4d 100644
--- a/clientapi/clientapi.go
+++ b/clientapi/clientapi.go
@@ -15,6 +15,9 @@
 package clientapi
 
 import (
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
 
@@ -25,13 +28,15 @@ import (
 	federationAPI "github.com/matrix-org/dendrite/federationapi/api"
 	"github.com/matrix-org/dendrite/internal/transactions"
 	roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 )
 
 // AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
 func AddPublicRoutes(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	routers httputil.Routers,
+	cfg *config.Dendrite,
+	natsInstance *jetstream.NATSInstance,
 	federation *gomatrixserverlib.FederationClient,
 	rsAPI roomserverAPI.ClientRoomserverAPI,
 	asAPI appserviceAPI.AppServiceInternalAPI,
@@ -39,27 +44,25 @@ func AddPublicRoutes(
 	fsAPI federationAPI.ClientFederationAPI,
 	userAPI userapi.ClientUserAPI,
 	userDirectoryProvider userapi.QuerySearchProfilesAPI,
-	extRoomsProvider api.ExtraPublicRoomsProvider,
+	extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool,
 ) {
-	cfg := &base.Cfg.ClientAPI
-	mscCfg := &base.Cfg.MSCs
-	js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
+	js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
 
 	syncProducer := &producers.SyncAPIProducer{
 		JetStream:              js,
-		TopicReceiptEvent:      cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent),
-		TopicSendToDeviceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
-		TopicTypingEvent:       cfg.Matrix.JetStream.Prefixed(jetstream.OutputTypingEvent),
-		TopicPresenceEvent:     cfg.Matrix.JetStream.Prefixed(jetstream.OutputPresenceEvent),
+		TopicReceiptEvent:      cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
+		TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
+		TopicTypingEvent:       cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
+		TopicPresenceEvent:     cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
 		UserAPI:                userAPI,
-		ServerName:             cfg.Matrix.ServerName,
+		ServerName:             cfg.Global.ServerName,
 	}
 
 	routing.Setup(
-		base,
+		routers,
 		cfg, rsAPI, asAPI,
 		userAPI, userDirectoryProvider, federation,
 		syncProducer, transactionsCache, fsAPI,
-		extRoomsProvider, mscCfg, natsClient,
+		extRoomsProvider, natsClient, enableMetrics,
 	)
 }
diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go
index 028d02e9..6a86980d 100644
--- a/clientapi/routing/routing.go
+++ b/clientapi/routing/routing.go
@@ -21,7 +21,6 @@ import (
 	"sync"
 
 	"github.com/gorilla/mux"
-	"github.com/matrix-org/dendrite/setup/base"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -50,8 +49,8 @@ import (
 // applied:
 // nolint: gocyclo
 func Setup(
-	base *base.BaseDendrite,
-	cfg *config.ClientAPI,
+	routers httputil.Routers,
+	dendriteCfg *config.Dendrite,
 	rsAPI roomserverAPI.ClientRoomserverAPI,
 	asAPI appserviceAPI.AppServiceInternalAPI,
 	userAPI userapi.ClientUserAPI,
@@ -61,14 +60,16 @@ func Setup(
 	transactionsCache *transactions.Cache,
 	federationSender federationAPI.ClientFederationAPI,
 	extRoomsProvider api.ExtraPublicRoomsProvider,
-	mscCfg *config.MSCs, natsClient *nats.Conn,
+	natsClient *nats.Conn, enableMetrics bool,
 ) {
-	publicAPIMux := base.PublicClientAPIMux
-	wkMux := base.PublicWellKnownAPIMux
-	synapseAdminRouter := base.SynapseAdminMux
-	dendriteAdminRouter := base.DendriteAdminMux
-
-	if base.EnableMetrics {
+	cfg := &dendriteCfg.ClientAPI
+	mscCfg := &dendriteCfg.MSCs
+	publicAPIMux := routers.Client
+	wkMux := routers.WellKnown
+	synapseAdminRouter := routers.SynapseAdmin
+	dendriteAdminRouter := routers.DendriteAdmin
+
+	if enableMetrics {
 		prometheus.MustRegister(amtRegUsers, sendEventDuration)
 	}
 
@@ -656,7 +657,7 @@ func Setup(
 	).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
 
 	v3mux.Handle("/auth/{authType}/fallback/web",
-		httputil.MakeHTMLAPI("auth_fallback", base.EnableMetrics, func(w http.ResponseWriter, req *http.Request) {
+		httputil.MakeHTMLAPI("auth_fallback", enableMetrics, func(w http.ResponseWriter, req *http.Request) {
 			vars := mux.Vars(req)
 			AuthFallback(w, req, vars["authType"], cfg)
 		}),
diff --git a/cmd/dendrite-demo-pinecone/conn/client.go b/cmd/dendrite-demo-pinecone/conn/client.go
index a91434f6..885de005 100644
--- a/cmd/dendrite-demo-pinecone/conn/client.go
+++ b/cmd/dendrite-demo-pinecone/conn/client.go
@@ -21,7 +21,7 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 	"nhooyr.io/websocket"
 
@@ -90,7 +90,7 @@ func createTransport(s *pineconeSessions.Sessions) *http.Transport {
 }
 
 func CreateClient(
-	base *base.BaseDendrite, s *pineconeSessions.Sessions,
+	s *pineconeSessions.Sessions,
 ) *gomatrixserverlib.Client {
 	return gomatrixserverlib.NewClient(
 		gomatrixserverlib.WithTransport(createTransport(s)),
@@ -98,10 +98,10 @@ func CreateClient(
 }
 
 func CreateFederationClient(
-	base *base.BaseDendrite, s *pineconeSessions.Sessions,
+	cfg *config.Dendrite, s *pineconeSessions.Sessions,
 ) *gomatrixserverlib.FederationClient {
 	return gomatrixserverlib.NewFederationClient(
-		base.Cfg.Global.SigningIdentities(),
+		cfg.Global.SigningIdentities(),
 		gomatrixserverlib.WithTransport(createTransport(s)),
 	)
 }
diff --git a/cmd/dendrite-demo-pinecone/main.go b/cmd/dendrite-demo-pinecone/main.go
index 7706a73f..7c710cbb 100644
--- a/cmd/dendrite-demo-pinecone/main.go
+++ b/cmd/dendrite-demo-pinecone/main.go
@@ -27,8 +27,11 @@ import (
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/monolith"
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
 	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/sirupsen/logrus"
 
@@ -87,9 +90,13 @@ func main() {
 		}
 	}
 
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
+
 	enableMetrics := true
 	enableWebsockets := true
-	p2pMonolith.SetupDendrite(cfg, *instancePort, *instanceRelayingEnabled, enableMetrics, enableWebsockets)
+	p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, *instancePort, *instanceRelayingEnabled, enableMetrics, enableWebsockets)
 	p2pMonolith.StartMonolith()
 	p2pMonolith.WaitForShutdown()
 
diff --git a/cmd/dendrite-demo-pinecone/monolith/monolith.go b/cmd/dendrite-demo-pinecone/monolith/monolith.go
index ea8e985c..10a3493e 100644
--- a/cmd/dendrite-demo-pinecone/monolith/monolith.go
+++ b/cmd/dendrite-demo-pinecone/monolith/monolith.go
@@ -23,6 +23,7 @@ import (
 	"net"
 	"net/http"
 	"path/filepath"
+	"sync"
 	"time"
 
 	"github.com/gorilla/mux"
@@ -37,7 +38,9 @@ import (
 	"github.com/matrix-org/dendrite/federationapi"
 	federationAPI "github.com/matrix-org/dendrite/federationapi/api"
 	"github.com/matrix-org/dendrite/federationapi/producers"
+	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/relayapi"
 	relayAPI "github.com/matrix-org/dendrite/relayapi/api"
 	"github.com/matrix-org/dendrite/roomserver"
@@ -45,6 +48,7 @@ import (
 	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/userapi"
 	userAPI "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
@@ -60,13 +64,13 @@ import (
 const SessionProtocol = "matrix"
 
 type P2PMonolith struct {
-	BaseDendrite   *base.BaseDendrite
 	Sessions       *pineconeSessions.Sessions
 	Multicast      *pineconeMulticast.Multicast
 	ConnManager    *pineconeConnections.ConnectionManager
 	Router         *pineconeRouter.Router
 	EventChannel   chan pineconeEvents.Event
 	RelayRetriever relay.RelayServerRetriever
+	ProcessCtx     *process.ProcessContext
 
 	dendrite           setup.Monolith
 	port               int
@@ -76,6 +80,7 @@ type P2PMonolith struct {
 	listener           net.Listener
 	httpListenAddr     string
 	stopHandlingEvents chan bool
+	httpServerMu       sync.Mutex
 }
 
 func GenerateDefaultConfig(sk ed25519.PrivateKey, storageDir string, cacheDir string, dbPrefix string) *config.Dendrite {
@@ -120,53 +125,52 @@ func (p *P2PMonolith) SetupPinecone(sk ed25519.PrivateKey) {
 	p.ConnManager = pineconeConnections.NewConnectionManager(p.Router, nil)
 }
 
-func (p *P2PMonolith) SetupDendrite(cfg *config.Dendrite, port int, enableRelaying bool, enableMetrics bool, enableWebsockets bool) {
-	if enableMetrics {
-		p.BaseDendrite = base.NewBaseDendrite(cfg)
-	} else {
-		p.BaseDendrite = base.NewBaseDendrite(cfg, base.DisableMetrics)
-	}
+func (p *P2PMonolith) SetupDendrite(
+	processCtx *process.ProcessContext, cfg *config.Dendrite, cm sqlutil.Connections, routers httputil.Routers,
+	port int, enableRelaying bool, enableMetrics bool, enableWebsockets bool) {
+
 	p.port = port
-	p.BaseDendrite.ConfigureAdminEndpoints()
+	base.ConfigureAdminEndpoints(processCtx, routers)
 
-	federation := conn.CreateFederationClient(p.BaseDendrite, p.Sessions)
+	federation := conn.CreateFederationClient(cfg, p.Sessions)
 
 	serverKeyAPI := &signing.YggdrasilKeys{}
 	keyRing := serverKeyAPI.KeyRing()
 
-	rsComponent := roomserver.NewInternalAPI(p.BaseDendrite)
-	rsAPI := rsComponent
+	caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, enableMetrics)
+	natsInstance := jetstream.NATSInstance{}
+	rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, enableMetrics)
 	fsAPI := federationapi.NewInternalAPI(
-		p.BaseDendrite, federation, rsAPI, p.BaseDendrite.Caches, keyRing, true,
+		processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
 	)
 
-	userAPI := userapi.NewInternalAPI(p.BaseDendrite, rsAPI, federation)
+	userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation)
 
-	asAPI := appservice.NewInternalAPI(p.BaseDendrite, userAPI, rsAPI)
+	asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
 
-	rsComponent.SetFederationAPI(fsAPI, keyRing)
+	rsAPI.SetFederationAPI(fsAPI, keyRing)
 
 	userProvider := users.NewPineconeUserProvider(p.Router, p.Sessions, userAPI, federation)
 	roomProvider := rooms.NewPineconeRoomProvider(p.Router, p.Sessions, fsAPI, federation)
 
-	js, _ := p.BaseDendrite.NATS.Prepare(p.BaseDendrite.ProcessContext, &p.BaseDendrite.Cfg.Global.JetStream)
+	js, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
 	producer := &producers.SyncAPIProducer{
 		JetStream:              js,
-		TopicReceiptEvent:      p.BaseDendrite.Cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
-		TopicSendToDeviceEvent: p.BaseDendrite.Cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
-		TopicTypingEvent:       p.BaseDendrite.Cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
-		TopicPresenceEvent:     p.BaseDendrite.Cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
-		TopicDeviceListUpdate:  p.BaseDendrite.Cfg.Global.JetStream.Prefixed(jetstream.InputDeviceListUpdate),
-		TopicSigningKeyUpdate:  p.BaseDendrite.Cfg.Global.JetStream.Prefixed(jetstream.InputSigningKeyUpdate),
-		Config:                 &p.BaseDendrite.Cfg.FederationAPI,
+		TopicReceiptEvent:      cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
+		TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
+		TopicTypingEvent:       cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
+		TopicPresenceEvent:     cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
+		TopicDeviceListUpdate:  cfg.Global.JetStream.Prefixed(jetstream.InputDeviceListUpdate),
+		TopicSigningKeyUpdate:  cfg.Global.JetStream.Prefixed(jetstream.InputSigningKeyUpdate),
+		Config:                 &cfg.FederationAPI,
 		UserAPI:                userAPI,
 	}
-	relayAPI := relayapi.NewRelayInternalAPI(p.BaseDendrite, federation, rsAPI, keyRing, producer, enableRelaying)
+	relayAPI := relayapi.NewRelayInternalAPI(cfg, cm, federation, rsAPI, keyRing, producer, enableRelaying, caches)
 	logrus.Infof("Relaying enabled: %v", relayAPI.RelayingEnabled())
 
 	p.dendrite = setup.Monolith{
-		Config:    p.BaseDendrite.Cfg,
-		Client:    conn.CreateClient(p.BaseDendrite, p.Sessions),
+		Config:    cfg,
+		Client:    conn.CreateClient(p.Sessions),
 		FedClient: federation,
 		KeyRing:   keyRing,
 
@@ -178,9 +182,10 @@ func (p *P2PMonolith) SetupDendrite(cfg *config.Dendrite, port int, enableRelayi
 		ExtPublicRoomsProvider:   roomProvider,
 		ExtUserDirectoryProvider: userProvider,
 	}
-	p.dendrite.AddAllPublicRoutes(p.BaseDendrite)
+	p.ProcessCtx = processCtx
+	p.dendrite.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, enableMetrics)
 
-	p.setupHttpServers(userProvider, enableWebsockets)
+	p.setupHttpServers(userProvider, routers, enableWebsockets)
 }
 
 func (p *P2PMonolith) GetFederationAPI() federationAPI.FederationInternalAPI {
@@ -202,20 +207,22 @@ func (p *P2PMonolith) StartMonolith() {
 
 func (p *P2PMonolith) Stop() {
 	logrus.Info("Stopping monolith")
-	_ = p.BaseDendrite.Close()
+	p.ProcessCtx.ShutdownDendrite()
 	p.WaitForShutdown()
 	logrus.Info("Stopped monolith")
 }
 
 func (p *P2PMonolith) WaitForShutdown() {
-	p.BaseDendrite.WaitForShutdown()
+	p.ProcessCtx.WaitForShutdown()
 	p.closeAllResources()
 }
 
 func (p *P2PMonolith) closeAllResources() {
 	logrus.Info("Closing monolith resources")
+	p.httpServerMu.Lock()
 	if p.httpServer != nil {
 		_ = p.httpServer.Shutdown(context.Background())
+		p.httpServerMu.Unlock()
 	}
 
 	select {
@@ -245,12 +252,12 @@ func (p *P2PMonolith) Addr() string {
 	return p.httpListenAddr
 }
 
-func (p *P2PMonolith) setupHttpServers(userProvider *users.PineconeUserProvider, enableWebsockets bool) {
+func (p *P2PMonolith) setupHttpServers(userProvider *users.PineconeUserProvider, routers httputil.Routers, enableWebsockets bool) {
 	p.httpMux = mux.NewRouter().SkipClean(true).UseEncodedPath()
-	p.httpMux.PathPrefix(httputil.PublicClientPathPrefix).Handler(p.BaseDendrite.PublicClientAPIMux)
-	p.httpMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(p.BaseDendrite.PublicMediaAPIMux)
-	p.httpMux.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(p.BaseDendrite.DendriteAdminMux)
-	p.httpMux.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(p.BaseDendrite.SynapseAdminMux)
+	p.httpMux.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
+	p.httpMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
+	p.httpMux.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
+	p.httpMux.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
 
 	if enableWebsockets {
 		wsUpgrader := websocket.Upgrader{
@@ -283,8 +290,8 @@ func (p *P2PMonolith) setupHttpServers(userProvider *users.PineconeUserProvider,
 
 	p.pineconeMux = mux.NewRouter().SkipClean(true).UseEncodedPath()
 	p.pineconeMux.PathPrefix(users.PublicURL).HandlerFunc(userProvider.FederatedUserProfiles)
-	p.pineconeMux.PathPrefix(httputil.PublicFederationPathPrefix).Handler(p.BaseDendrite.PublicFederationAPIMux)
-	p.pineconeMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(p.BaseDendrite.PublicMediaAPIMux)
+	p.pineconeMux.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
+	p.pineconeMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
 
 	pHTTP := p.Sessions.Protocol(SessionProtocol).HTTP()
 	pHTTP.Mux().Handle(users.PublicURL, p.pineconeMux)
@@ -294,6 +301,7 @@ func (p *P2PMonolith) setupHttpServers(userProvider *users.PineconeUserProvider,
 
 func (p *P2PMonolith) startHTTPServers() {
 	go func() {
+		p.httpServerMu.Lock()
 		// Build both ends of a HTTP multiplex.
 		p.httpServer = &http.Server{
 			Addr:         ":0",
@@ -306,7 +314,7 @@ func (p *P2PMonolith) startHTTPServers() {
 			},
 			Handler: p.pineconeMux,
 		}
-
+		p.httpServerMu.Unlock()
 		pubkey := p.Router.PublicKey()
 		pubkeyString := hex.EncodeToString(pubkey[:])
 		logrus.Info("Listening on ", pubkeyString)
@@ -368,7 +376,7 @@ func (p *P2PMonolith) startEventHandler() {
 						ServerNames: []gomatrixserverlib.ServerName{gomatrixserverlib.ServerName(e.PeerID)},
 					}
 					res := &federationAPI.PerformWakeupServersResponse{}
-					if err := p.dendrite.FederationAPI.PerformWakeupServers(p.BaseDendrite.Context(), req, res); err != nil {
+					if err := p.dendrite.FederationAPI.PerformWakeupServers(p.ProcessCtx.Context(), req, res); err != nil {
 						eLog.WithError(err).Error("Failed to wakeup destination", e.PeerID)
 					}
 				}
diff --git a/cmd/dendrite-demo-yggdrasil/main.go b/cmd/dendrite-demo-yggdrasil/main.go
index d759c6a7..9a195990 100644
--- a/cmd/dendrite-demo-yggdrasil/main.go
+++ b/cmd/dendrite-demo-yggdrasil/main.go
@@ -27,6 +27,11 @@ import (
 	"path/filepath"
 	"time"
 
+	"github.com/getsentry/sentry-go"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/gomatrixserverlib"
 
 	"github.com/gorilla/mux"
@@ -41,7 +46,7 @@ import (
 	"github.com/matrix-org/dendrite/internal/httputil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/setup"
-	"github.com/matrix-org/dendrite/setup/base"
+	basepkg "github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/mscs"
 	"github.com/matrix-org/dendrite/test"
@@ -57,6 +62,7 @@ var (
 	instanceDir    = flag.String("dir", ".", "the directory to store the databases in (if --config not specified)")
 )
 
+// nolint: gocyclo
 func main() {
 	flag.Parse()
 	internal.SetupPprof()
@@ -142,37 +148,83 @@ func main() {
 	cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
 	cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
 
-	base := base.NewBaseDendrite(cfg)
-	base.ConfigureAdminEndpoints()
-	defer base.Close() // nolint: errcheck
+	configErrors := &config.ConfigErrors{}
+	cfg.Verify(configErrors)
+	if len(*configErrors) > 0 {
+		for _, err := range *configErrors {
+			logrus.Errorf("Configuration error: %s", err)
+		}
+		logrus.Fatalf("Failed to start due to configuration errors")
+	}
+
+	internal.SetupStdLogging()
+	internal.SetupHookLogging(cfg.Logging)
+	internal.SetupPprof()
+
+	logrus.Infof("Dendrite version %s", internal.VersionString())
+
+	if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
+		logrus.Warn("Open registration is enabled")
+	}
+
+	closer, err := cfg.SetupTracing()
+	if err != nil {
+		logrus.WithError(err).Panicf("failed to start opentracing")
+	}
+	defer closer.Close() // nolint: errcheck
+
+	if cfg.Global.Sentry.Enabled {
+		logrus.Info("Setting up Sentry for debugging...")
+		err = sentry.Init(sentry.ClientOptions{
+			Dsn:              cfg.Global.Sentry.DSN,
+			Environment:      cfg.Global.Sentry.Environment,
+			Debug:            true,
+			ServerName:       string(cfg.Global.ServerName),
+			Release:          "dendrite@" + internal.VersionString(),
+			AttachStacktrace: true,
+		})
+		if err != nil {
+			logrus.WithError(err).Panic("failed to start Sentry")
+		}
+	}
+
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
+
+	basepkg.ConfigureAdminEndpoints(processCtx, routers)
+	defer func() {
+		processCtx.ShutdownDendrite()
+		processCtx.WaitForShutdown()
+	}() // nolint: errcheck
 
 	ygg, err := yggconn.Setup(sk, *instanceName, ".", *instancePeer, *instanceListen)
 	if err != nil {
 		panic(err)
 	}
 
-	federation := ygg.CreateFederationClient(base)
+	federation := ygg.CreateFederationClient(cfg)
 
 	serverKeyAPI := &signing.YggdrasilKeys{}
 	keyRing := serverKeyAPI.KeyRing()
 
-	rsAPI := roomserver.NewInternalAPI(
-		base,
-	)
+	caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
+	natsInstance := jetstream.NATSInstance{}
+	rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
 
-	userAPI := userapi.NewInternalAPI(base, rsAPI, federation)
+	userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation)
 
-	asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
+	asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
 	rsAPI.SetAppserviceAPI(asAPI)
 	fsAPI := federationapi.NewInternalAPI(
-		base, federation, rsAPI, base.Caches, keyRing, true,
+		processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
 	)
 
 	rsAPI.SetFederationAPI(fsAPI, keyRing)
 
 	monolith := setup.Monolith{
-		Config:    base.Cfg,
-		Client:    ygg.CreateClient(base),
+		Config:    cfg,
+		Client:    ygg.CreateClient(),
 		FedClient: federation,
 		KeyRing:   keyRing,
 
@@ -184,21 +236,21 @@ func main() {
 			ygg, fsAPI, federation,
 		),
 	}
-	monolith.AddAllPublicRoutes(base)
-	if err := mscs.Enable(base, &monolith); err != nil {
+	monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
+	if err := mscs.Enable(cfg, cm, routers, &monolith, caches); err != nil {
 		logrus.WithError(err).Fatalf("Failed to enable MSCs")
 	}
 
 	httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
-	httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
-	httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
-	httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(base.DendriteAdminMux)
-	httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(base.SynapseAdminMux)
+	httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
+	httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
+	httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
+	httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
 	embed.Embed(httpRouter, *instancePort, "Yggdrasil Demo")
 
 	yggRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
-	yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
-	yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
+	yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
+	yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
 
 	// Build both ends of a HTTP multiplex.
 	httpServer := &http.Server{
@@ -232,5 +284,5 @@ func main() {
 	}()
 
 	// We want to block forever to let the HTTP and HTTPS handler serve the APIs
-	base.WaitForShutdown()
+	basepkg.WaitForShutdown(processCtx)
 }
diff --git a/cmd/dendrite-demo-yggdrasil/yggconn/client.go b/cmd/dendrite-demo-yggdrasil/yggconn/client.go
index 41a9ec12..51365547 100644
--- a/cmd/dendrite-demo-yggdrasil/yggconn/client.go
+++ b/cmd/dendrite-demo-yggdrasil/yggconn/client.go
@@ -4,7 +4,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
@@ -17,9 +17,7 @@ func (y *yggroundtripper) RoundTrip(req *http.Request) (*http.Response, error) {
 	return y.inner.RoundTrip(req)
 }
 
-func (n *Node) CreateClient(
-	base *base.BaseDendrite,
-) *gomatrixserverlib.Client {
+func (n *Node) CreateClient() *gomatrixserverlib.Client {
 	tr := &http.Transport{}
 	tr.RegisterProtocol(
 		"matrix", &yggroundtripper{
@@ -39,7 +37,7 @@ func (n *Node) CreateClient(
 }
 
 func (n *Node) CreateFederationClient(
-	base *base.BaseDendrite,
+	cfg *config.Dendrite,
 ) *gomatrixserverlib.FederationClient {
 	tr := &http.Transport{}
 	tr.RegisterProtocol(
@@ -55,7 +53,7 @@ func (n *Node) CreateFederationClient(
 		},
 	)
 	return gomatrixserverlib.NewFederationClient(
-		base.Cfg.Global.SigningIdentities(),
+		cfg.Global.SigningIdentities(),
 		gomatrixserverlib.WithTransport(tr),
 	)
 }
diff --git a/cmd/dendrite/main.go b/cmd/dendrite/main.go
index 1ae348cf..a22677eb 100644
--- a/cmd/dendrite/main.go
+++ b/cmd/dendrite/main.go
@@ -16,8 +16,16 @@ package main
 
 import (
 	"flag"
-	"io/fs"
-
+	"time"
+
+	"github.com/getsentry/sentry-go"
+	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
+	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/sirupsen/logrus"
 
 	"github.com/matrix-org/dendrite/appservice"
@@ -34,8 +42,8 @@ var (
 	unixSocket = flag.String("unix-socket", "",
 		"EXPERIMENTAL(unstable): The HTTP listening unix socket for the server (disables http[s]-bind-address feature)",
 	)
-	unixSocketPermission = flag.Int("unix-socket-permission", 0755,
-		"EXPERIMENTAL(unstable): The HTTP listening unix socket permission for the server",
+	unixSocketPermission = flag.String("unix-socket-permission", "755",
+		"EXPERIMENTAL(unstable): The HTTP listening unix socket permission for the server (in chmod format like 755)",
 	)
 	httpBindAddr  = flag.String("http-bind-address", ":8008", "The HTTP listening port for the server")
 	httpsBindAddr = flag.String("https-bind-address", ":8448", "The HTTPS listening port for the server")
@@ -59,27 +67,97 @@ func main() {
 		}
 		httpsAddr = https
 	} else {
-		httpAddr = config.UnixSocketAddress(*unixSocket, fs.FileMode(*unixSocketPermission))
+		socket, err := config.UnixSocketAddress(*unixSocket, *unixSocketPermission)
+		if err != nil {
+			logrus.WithError(err).Fatalf("Failed to parse unix socket")
+		}
+		httpAddr = socket
+	}
+
+	configErrors := &config.ConfigErrors{}
+	cfg.Verify(configErrors)
+	if len(*configErrors) > 0 {
+		for _, err := range *configErrors {
+			logrus.Errorf("Configuration error: %s", err)
+		}
+		logrus.Fatalf("Failed to start due to configuration errors")
+	}
+	processCtx := process.NewProcessContext()
+
+	internal.SetupStdLogging()
+	internal.SetupHookLogging(cfg.Logging)
+	internal.SetupPprof()
+
+	basepkg.PlatformSanityChecks()
+
+	logrus.Infof("Dendrite version %s", internal.VersionString())
+	if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
+		logrus.Warn("Open registration is enabled")
 	}
 
-	options := []basepkg.BaseDendriteOptions{}
+	// create DNS cache
+	var dnsCache *gomatrixserverlib.DNSCache
+	if cfg.Global.DNSCache.Enabled {
+		dnsCache = gomatrixserverlib.NewDNSCache(
+			cfg.Global.DNSCache.CacheSize,
+			cfg.Global.DNSCache.CacheLifetime,
+		)
+		logrus.Infof(
+			"DNS cache enabled (size %d, lifetime %s)",
+			cfg.Global.DNSCache.CacheSize,
+			cfg.Global.DNSCache.CacheLifetime,
+		)
+	}
 
-	base := basepkg.NewBaseDendrite(cfg, options...)
-	defer base.Close() // nolint: errcheck
+	// setup tracing
+	closer, err := cfg.SetupTracing()
+	if err != nil {
+		logrus.WithError(err).Panicf("failed to start opentracing")
+	}
+	defer closer.Close() // nolint: errcheck
+
+	// setup sentry
+	if cfg.Global.Sentry.Enabled {
+		logrus.Info("Setting up Sentry for debugging...")
+		err = sentry.Init(sentry.ClientOptions{
+			Dsn:              cfg.Global.Sentry.DSN,
+			Environment:      cfg.Global.Sentry.Environment,
+			Debug:            true,
+			ServerName:       string(cfg.Global.ServerName),
+			Release:          "dendrite@" + internal.VersionString(),
+			AttachStacktrace: true,
+		})
+		if err != nil {
+			logrus.WithError(err).Panic("failed to start Sentry")
+		}
+		go func() {
+			processCtx.ComponentStarted()
+			<-processCtx.WaitForShutdown()
+			if !sentry.Flush(time.Second * 5) {
+				logrus.Warnf("failed to flush all Sentry events!")
+			}
+			processCtx.ComponentFinished()
+		}()
+	}
 
-	federation := base.CreateFederationClient()
+	federationClient := basepkg.CreateFederationClient(cfg, dnsCache)
+	httpClient := basepkg.CreateClient(cfg, dnsCache)
 
-	rsAPI := roomserver.NewInternalAPI(base)
+	// prepare required dependencies
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
 
+	caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
+	natsInstance := jetstream.NATSInstance{}
+	rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
 	fsAPI := federationapi.NewInternalAPI(
-		base, federation, rsAPI, base.Caches, nil, false,
+		processCtx, cfg, cm, &natsInstance, federationClient, rsAPI, caches, nil, false,
 	)
 
 	keyRing := fsAPI.KeyRing()
 
-	userAPI := userapi.NewInternalAPI(base, rsAPI, federation)
-
-	asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
+	userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federationClient)
+	asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
 
 	// The underlying roomserver implementation needs to be able to call the fedsender.
 	// This is different to rsAPI which can be the http client which doesn't need this
@@ -89,9 +167,9 @@ func main() {
 	rsAPI.SetUserAPI(userAPI)
 
 	monolith := setup.Monolith{
-		Config:    base.Cfg,
-		Client:    base.CreateClient(),
-		FedClient: federation,
+		Config:    cfg,
+		Client:    httpClient,
+		FedClient: federationClient,
 		KeyRing:   keyRing,
 
 		AppserviceAPI: asAPI,
@@ -101,25 +179,25 @@ func main() {
 		RoomserverAPI: rsAPI,
 		UserAPI:       userAPI,
 	}
-	monolith.AddAllPublicRoutes(base)
+	monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
 
-	if len(base.Cfg.MSCs.MSCs) > 0 {
-		if err := mscs.Enable(base, &monolith); err != nil {
+	if len(cfg.MSCs.MSCs) > 0 {
+		if err := mscs.Enable(cfg, cm, routers, &monolith, caches); err != nil {
 			logrus.WithError(err).Fatalf("Failed to enable MSCs")
 		}
 	}
 
 	// Expose the matrix APIs directly rather than putting them under a /api path.
 	go func() {
-		base.SetupAndServeHTTP(httpAddr, nil, nil)
+		basepkg.SetupAndServeHTTP(processCtx, cfg, routers, httpAddr, nil, nil)
 	}()
 	// Handle HTTPS if certificate and key are provided
 	if *unixSocket == "" && *certFile != "" && *keyFile != "" {
 		go func() {
-			base.SetupAndServeHTTP(httpsAddr, certFile, keyFile)
+			basepkg.SetupAndServeHTTP(processCtx, cfg, routers, httpsAddr, certFile, keyFile)
 		}()
 	}
 
 	// We want to block forever to let the HTTP and HTTPS handler serve the APIs
-	base.WaitForShutdown()
+	basepkg.WaitForShutdown(processCtx)
 }
diff --git a/cmd/resolve-state/main.go b/cmd/resolve-state/main.go
index a9cc80cb..09c0e690 100644
--- a/cmd/resolve-state/main.go
+++ b/cmd/resolve-state/main.go
@@ -10,12 +10,13 @@ import (
 	"time"
 
 	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver/state"
 	"github.com/matrix-org/dendrite/roomserver/storage"
 	"github.com/matrix-org/dendrite/roomserver/types"
 	"github.com/matrix-org/dendrite/setup"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
@@ -40,7 +41,7 @@ func main() {
 		Level: "error",
 	})
 	cfg.ClientAPI.RegistrationDisabled = true
-	base := base.NewBaseDendrite(cfg, base.DisableMetrics)
+
 	args := flag.Args()
 
 	fmt.Println("Room version", *roomVersion)
@@ -54,8 +55,10 @@ func main() {
 
 	fmt.Println("Fetching", len(snapshotNIDs), "snapshot NIDs")
 
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
 	roomserverDB, err := storage.Open(
-		base, &cfg.RoomServer.Database,
+		processCtx.Context(), cm, &cfg.RoomServer.Database,
 		caching.NewRistrettoCache(128*1024*1024, time.Hour, true),
 	)
 	if err != nil {
diff --git a/docs/installation/7_configuration.md b/docs/installation/7_configuration.md
index 5f123bfc..0cc67b15 100644
--- a/docs/installation/7_configuration.md
+++ b/docs/installation/7_configuration.md
@@ -10,7 +10,7 @@ permalink: /installation/configuration
 A YAML configuration file is used to configure Dendrite. A sample configuration file is
 present in the top level of the Dendrite repository:
 
-* [`dendrite-sample.monolith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.monolith.yaml)
+* [`dendrite-sample.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.yaml)
 
 You will need to duplicate the sample, calling it `dendrite.yaml` for example, and then
 tailor it to your installation. At a minimum, you will need to populate the following
@@ -86,9 +86,8 @@ that you chose.
 
 ### Global connection pool
 
-If you are running a monolith deployment and want to use a single connection pool to a
-single PostgreSQL database, then you must uncomment and configure the `database` section
-within the `global` section:
+If you want to use a single connection pool to a single PostgreSQL database, then you must
+uncomment and configure the `database` section within the `global` section:
 
 ```yaml
 global:
@@ -102,15 +101,15 @@ global:
 
 **You must then remove or comment out** the `database` sections from other areas of the
 configuration file, e.g. under the `app_service_api`, `federation_api`, `key_server`,
-`media_api`, `mscs`, `room_server`, `sync_api` and `user_api` blocks, otherwise these will
-override the `global` database configuration.
+`media_api`, `mscs`, `relay_api`, `room_server`, `sync_api` and `user_api` blocks, otherwise
+these will override the `global` database configuration.
 
 ### Per-component connections (all other configurations)
 
 If you are are using SQLite databases or separate PostgreSQL
 databases per component, then you must instead configure the `database` sections under each
 of the component blocks ,e.g. under the `app_service_api`, `federation_api`, `key_server`,
-`media_api`, `mscs`, `room_server`, `sync_api` and `user_api` blocks.
+`media_api`, `mscs`, `relay_api`, `room_server`, `sync_api` and `user_api` blocks.
 
 For example, with PostgreSQL:
 
diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go
index ec482659..3b5394a1 100644
--- a/federationapi/federationapi.go
+++ b/federationapi/federationapi.go
@@ -17,6 +17,10 @@ package federationapi
 import (
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/sirupsen/logrus"
 
 	"github.com/matrix-org/dendrite/federationapi/api"
@@ -29,7 +33,6 @@ import (
 	"github.com/matrix-org/dendrite/federationapi/storage"
 	"github.com/matrix-org/dendrite/internal/caching"
 	roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 
@@ -40,17 +43,21 @@ import (
 
 // AddPublicRoutes sets up and registers HTTP handlers on the base API muxes for the FederationAPI component.
 func AddPublicRoutes(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	routers httputil.Routers,
+	dendriteConfig *config.Dendrite,
+	natsInstance *jetstream.NATSInstance,
 	userAPI userapi.FederationUserAPI,
 	federation *gomatrixserverlib.FederationClient,
 	keyRing gomatrixserverlib.JSONVerifier,
 	rsAPI roomserverAPI.FederationRoomserverAPI,
 	fedAPI federationAPI.FederationInternalAPI,
 	servers federationAPI.ServersInRoomProvider,
+	enableMetrics bool,
 ) {
-	cfg := &base.Cfg.FederationAPI
-	mscCfg := &base.Cfg.MSCs
-	js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
+	cfg := &dendriteConfig.FederationAPI
+	mscCfg := &dendriteConfig.MSCs
+	js, _ := natsInstance.Prepare(processContext, &cfg.Matrix.JetStream)
 	producer := &producers.SyncAPIProducer{
 		JetStream:              js,
 		TopicReceiptEvent:      cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent),
@@ -75,26 +82,30 @@ func AddPublicRoutes(
 	}
 
 	routing.Setup(
-		base,
+		routers,
+		dendriteConfig,
 		rsAPI, f, keyRing,
 		federation, userAPI, mscCfg,
-		servers, producer,
+		servers, producer, enableMetrics,
 	)
 }
 
 // NewInternalAPI returns a concerete implementation of the internal API. Callers
 // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
 func NewInternalAPI(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	dendriteCfg *config.Dendrite,
+	cm sqlutil.Connections,
+	natsInstance *jetstream.NATSInstance,
 	federation api.FederationClient,
 	rsAPI roomserverAPI.FederationRoomserverAPI,
 	caches *caching.Caches,
 	keyRing *gomatrixserverlib.KeyRing,
 	resetBlacklist bool,
 ) api.FederationInternalAPI {
-	cfg := &base.Cfg.FederationAPI
+	cfg := &dendriteCfg.FederationAPI
 
-	federationDB, err := storage.NewDatabase(base, &cfg.Database, base.Caches, base.Cfg.Global.IsLocalServerName)
+	federationDB, err := storage.NewDatabase(processContext.Context(), cm, &cfg.Database, caches, dendriteCfg.Global.IsLocalServerName)
 	if err != nil {
 		logrus.WithError(err).Panic("failed to connect to federation sender db")
 	}
@@ -108,51 +119,51 @@ func NewInternalAPI(
 		cfg.FederationMaxRetries+1,
 		cfg.P2PFederationRetriesUntilAssumedOffline+1)
 
-	js, nats := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
+	js, nats := natsInstance.Prepare(processContext, &cfg.Matrix.JetStream)
 
-	signingInfo := base.Cfg.Global.SigningIdentities()
+	signingInfo := dendriteCfg.Global.SigningIdentities()
 
 	queues := queue.NewOutgoingQueues(
-		federationDB, base.ProcessContext,
+		federationDB, processContext,
 		cfg.Matrix.DisableFederation,
 		cfg.Matrix.ServerName, federation, rsAPI, &stats,
 		signingInfo,
 	)
 
 	rsConsumer := consumers.NewOutputRoomEventConsumer(
-		base.ProcessContext, cfg, js, nats, queues,
+		processContext, cfg, js, nats, queues,
 		federationDB, rsAPI,
 	)
 	if err = rsConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start room server consumer")
 	}
 	tsConsumer := consumers.NewOutputSendToDeviceConsumer(
-		base.ProcessContext, cfg, js, queues, federationDB,
+		processContext, cfg, js, queues, federationDB,
 	)
 	if err = tsConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start send-to-device consumer")
 	}
 	receiptConsumer := consumers.NewOutputReceiptConsumer(
-		base.ProcessContext, cfg, js, queues, federationDB,
+		processContext, cfg, js, queues, federationDB,
 	)
 	if err = receiptConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start receipt consumer")
 	}
 	typingConsumer := consumers.NewOutputTypingConsumer(
-		base.ProcessContext, cfg, js, queues, federationDB,
+		processContext, cfg, js, queues, federationDB,
 	)
 	if err = typingConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start typing consumer")
 	}
 	keyConsumer := consumers.NewKeyChangeConsumer(
-		base.ProcessContext, &base.Cfg.KeyServer, js, queues, federationDB, rsAPI,
+		processContext, &dendriteCfg.KeyServer, js, queues, federationDB, rsAPI,
 	)
 	if err = keyConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start key server consumer")
 	}
 
 	presenceConsumer := consumers.NewOutputPresenceConsumer(
-		base.ProcessContext, cfg, js, queues, federationDB, rsAPI,
+		processContext, cfg, js, queues, federationDB, rsAPI,
 	)
 	if err = presenceConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start presence consumer")
@@ -161,7 +172,7 @@ func NewInternalAPI(
 	var cleanExpiredEDUs func()
 	cleanExpiredEDUs = func() {
 		logrus.Infof("Cleaning expired EDUs")
-		if err := federationDB.DeleteExpiredEDUs(base.Context()); err != nil {
+		if err := federationDB.DeleteExpiredEDUs(processContext.Context()); err != nil {
 			logrus.WithError(err).Error("Failed to clean expired EDUs")
 		}
 		time.AfterFunc(time.Hour, cleanExpiredEDUs)
diff --git a/federationapi/routing/routing.go b/federationapi/routing/routing.go
index 324740dd..a1f943e7 100644
--- a/federationapi/routing/routing.go
+++ b/federationapi/routing/routing.go
@@ -31,7 +31,6 @@ import (
 	"github.com/matrix-org/dendrite/internal/httputil"
 	"github.com/matrix-org/dendrite/roomserver/api"
 	roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
@@ -55,7 +54,8 @@ const (
 // applied:
 // nolint: gocyclo
 func Setup(
-	base *base.BaseDendrite,
+	routers httputil.Routers,
+	dendriteCfg *config.Dendrite,
 	rsAPI roomserverAPI.FederationRoomserverAPI,
 	fsAPI *fedInternal.FederationInternalAPI,
 	keys gomatrixserverlib.JSONVerifier,
@@ -63,14 +63,14 @@ func Setup(
 	userAPI userapi.FederationUserAPI,
 	mscCfg *config.MSCs,
 	servers federationAPI.ServersInRoomProvider,
-	producer *producers.SyncAPIProducer,
+	producer *producers.SyncAPIProducer, enableMetrics bool,
 ) {
-	fedMux := base.PublicFederationAPIMux
-	keyMux := base.PublicKeyAPIMux
-	wkMux := base.PublicWellKnownAPIMux
-	cfg := &base.Cfg.FederationAPI
+	fedMux := routers.Federation
+	keyMux := routers.Keys
+	wkMux := routers.WellKnown
+	cfg := &dendriteCfg.FederationAPI
 
-	if base.EnableMetrics {
+	if enableMetrics {
 		prometheus.MustRegister(
 			internal.PDUCountTotal, internal.EDUCountTotal,
 		)
diff --git a/federationapi/storage/postgres/blacklist_table.go b/federationapi/storage/postgres/blacklist_table.go
index eef37318..1d931daa 100644
--- a/federationapi/storage/postgres/blacklist_table.go
+++ b/federationapi/storage/postgres/blacklist_table.go
@@ -60,19 +60,12 @@ func NewPostgresBlacklistTable(db *sql.DB) (s *blacklistStatements, err error) {
 		return
 	}
 
-	if s.insertBlacklistStmt, err = db.Prepare(insertBlacklistSQL); err != nil {
-		return
-	}
-	if s.selectBlacklistStmt, err = db.Prepare(selectBlacklistSQL); err != nil {
-		return
-	}
-	if s.deleteBlacklistStmt, err = db.Prepare(deleteBlacklistSQL); err != nil {
-		return
-	}
-	if s.deleteAllBlacklistStmt, err = db.Prepare(deleteAllBlacklistSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertBlacklistStmt, insertBlacklistSQL},
+		{&s.selectBlacklistStmt, selectBlacklistSQL},
+		{&s.deleteBlacklistStmt, deleteBlacklistSQL},
+		{&s.deleteAllBlacklistStmt, deleteAllBlacklistSQL},
+	}.Prepare(db)
 }
 
 func (s *blacklistStatements) InsertBlacklist(
diff --git a/federationapi/storage/postgres/joined_hosts_table.go b/federationapi/storage/postgres/joined_hosts_table.go
index 9a397756..8806db55 100644
--- a/federationapi/storage/postgres/joined_hosts_table.go
+++ b/federationapi/storage/postgres/joined_hosts_table.go
@@ -90,28 +90,15 @@ func NewPostgresJoinedHostsTable(db *sql.DB) (s *joinedHostsStatements, err erro
 	if err != nil {
 		return
 	}
-	if s.insertJoinedHostsStmt, err = s.db.Prepare(insertJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.deleteJoinedHostsStmt, err = s.db.Prepare(deleteJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.deleteJoinedHostsForRoomStmt, err = s.db.Prepare(deleteJoinedHostsForRoomSQL); err != nil {
-		return
-	}
-	if s.selectJoinedHostsStmt, err = s.db.Prepare(selectJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.selectAllJoinedHostsStmt, err = s.db.Prepare(selectAllJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.selectJoinedHostsForRoomsStmt, err = s.db.Prepare(selectJoinedHostsForRoomsSQL); err != nil {
-		return
-	}
-	if s.selectJoinedHostsForRoomsExcludingBlacklistedStmt, err = s.db.Prepare(selectJoinedHostsForRoomsExcludingBlacklistedSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertJoinedHostsStmt, insertJoinedHostsSQL},
+		{&s.deleteJoinedHostsStmt, deleteJoinedHostsSQL},
+		{&s.deleteJoinedHostsForRoomStmt, deleteJoinedHostsForRoomSQL},
+		{&s.selectJoinedHostsStmt, selectJoinedHostsSQL},
+		{&s.selectAllJoinedHostsStmt, selectAllJoinedHostsSQL},
+		{&s.selectJoinedHostsForRoomsStmt, selectJoinedHostsForRoomsSQL},
+		{&s.selectJoinedHostsForRoomsExcludingBlacklistedStmt, selectJoinedHostsForRoomsExcludingBlacklistedSQL},
+	}.Prepare(db)
 }
 
 func (s *joinedHostsStatements) InsertJoinedHosts(
diff --git a/federationapi/storage/postgres/notary_server_keys_json_table.go b/federationapi/storage/postgres/notary_server_keys_json_table.go
index 65221c08..9fc93a61 100644
--- a/federationapi/storage/postgres/notary_server_keys_json_table.go
+++ b/federationapi/storage/postgres/notary_server_keys_json_table.go
@@ -19,6 +19,7 @@ import (
 	"database/sql"
 
 	"github.com/matrix-org/dendrite/federationapi/storage/tables"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
@@ -50,10 +51,9 @@ func NewPostgresNotaryServerKeysTable(db *sql.DB) (s *notaryServerKeysStatements
 		return
 	}
 
-	if s.insertServerKeysJSONStmt, err = db.Prepare(insertServerKeysJSONSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertServerKeysJSONStmt, insertServerKeysJSONSQL},
+	}.Prepare(db)
 }
 
 func (s *notaryServerKeysStatements) InsertJSONResponse(
diff --git a/federationapi/storage/postgres/notary_server_keys_metadata_table.go b/federationapi/storage/postgres/notary_server_keys_metadata_table.go
index 72faf480..6d38ccab 100644
--- a/federationapi/storage/postgres/notary_server_keys_metadata_table.go
+++ b/federationapi/storage/postgres/notary_server_keys_metadata_table.go
@@ -22,6 +22,7 @@ import (
 	"github.com/lib/pq"
 	"github.com/matrix-org/dendrite/federationapi/storage/tables"
 	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
@@ -91,22 +92,13 @@ func NewPostgresNotaryServerKeysMetadataTable(db *sql.DB) (s *notaryServerKeysMe
 		return
 	}
 
-	if s.upsertServerKeysStmt, err = db.Prepare(upsertServerKeysSQL); err != nil {
-		return
-	}
-	if s.selectNotaryKeyResponsesStmt, err = db.Prepare(selectNotaryKeyResponsesSQL); err != nil {
-		return
-	}
-	if s.selectNotaryKeyResponsesWithKeyIDsStmt, err = db.Prepare(selectNotaryKeyResponsesWithKeyIDsSQL); err != nil {
-		return
-	}
-	if s.selectNotaryKeyMetadataStmt, err = db.Prepare(selectNotaryKeyMetadataSQL); err != nil {
-		return
-	}
-	if s.deleteUnusedServerKeysJSONStmt, err = db.Prepare(deleteUnusedServerKeysJSONSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.upsertServerKeysStmt, upsertServerKeysSQL},
+		{&s.selectNotaryKeyResponsesStmt, selectNotaryKeyResponsesSQL},
+		{&s.selectNotaryKeyResponsesWithKeyIDsStmt, selectNotaryKeyResponsesWithKeyIDsSQL},
+		{&s.selectNotaryKeyMetadataStmt, selectNotaryKeyMetadataSQL},
+		{&s.deleteUnusedServerKeysJSONStmt, deleteUnusedServerKeysJSONSQL},
+	}.Prepare(db)
 }
 
 func (s *notaryServerKeysMetadataStatements) UpsertKey(
diff --git a/federationapi/storage/postgres/queue_json_table.go b/federationapi/storage/postgres/queue_json_table.go
index e3307418..563738dd 100644
--- a/federationapi/storage/postgres/queue_json_table.go
+++ b/federationapi/storage/postgres/queue_json_table.go
@@ -65,16 +65,11 @@ func NewPostgresQueueJSONTable(db *sql.DB) (s *queueJSONStatements, err error) {
 	if err != nil {
 		return
 	}
-	if s.insertJSONStmt, err = s.db.Prepare(insertJSONSQL); err != nil {
-		return
-	}
-	if s.deleteJSONStmt, err = s.db.Prepare(deleteJSONSQL); err != nil {
-		return
-	}
-	if s.selectJSONStmt, err = s.db.Prepare(selectJSONSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertJSONStmt, insertJSONSQL},
+		{&s.deleteJSONStmt, deleteJSONSQL},
+		{&s.selectJSONStmt, selectJSONSQL},
+	}.Prepare(db)
 }
 
 func (s *queueJSONStatements) InsertQueueJSON(
diff --git a/federationapi/storage/postgres/queue_pdus_table.go b/federationapi/storage/postgres/queue_pdus_table.go
index 3b0bef9a..b97be482 100644
--- a/federationapi/storage/postgres/queue_pdus_table.go
+++ b/federationapi/storage/postgres/queue_pdus_table.go
@@ -78,22 +78,13 @@ func NewPostgresQueuePDUsTable(db *sql.DB) (s *queuePDUsStatements, err error) {
 	if err != nil {
 		return
 	}
-	if s.insertQueuePDUStmt, err = s.db.Prepare(insertQueuePDUSQL); err != nil {
-		return
-	}
-	if s.deleteQueuePDUsStmt, err = s.db.Prepare(deleteQueuePDUSQL); err != nil {
-		return
-	}
-	if s.selectQueuePDUsStmt, err = s.db.Prepare(selectQueuePDUsSQL); err != nil {
-		return
-	}
-	if s.selectQueuePDUReferenceJSONCountStmt, err = s.db.Prepare(selectQueuePDUReferenceJSONCountSQL); err != nil {
-		return
-	}
-	if s.selectQueuePDUServerNamesStmt, err = s.db.Prepare(selectQueuePDUServerNamesSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertQueuePDUStmt, insertQueuePDUSQL},
+		{&s.deleteQueuePDUsStmt, deleteQueuePDUSQL},
+		{&s.selectQueuePDUsStmt, selectQueuePDUsSQL},
+		{&s.selectQueuePDUReferenceJSONCountStmt, selectQueuePDUReferenceJSONCountSQL},
+		{&s.selectQueuePDUServerNamesStmt, selectQueuePDUServerNamesSQL},
+	}.Prepare(db)
 }
 
 func (s *queuePDUsStatements) InsertQueuePDU(
diff --git a/federationapi/storage/postgres/server_key_table.go b/federationapi/storage/postgres/server_key_table.go
index 16e294e0..f393351b 100644
--- a/federationapi/storage/postgres/server_key_table.go
+++ b/federationapi/storage/postgres/server_key_table.go
@@ -72,13 +72,10 @@ func NewPostgresServerSigningKeysTable(db *sql.DB) (s *serverSigningKeyStatement
 	if err != nil {
 		return
 	}
-	if s.bulkSelectServerKeysStmt, err = db.Prepare(bulkSelectServerSigningKeysSQL); err != nil {
-		return
-	}
-	if s.upsertServerKeysStmt, err = db.Prepare(upsertServerSigningKeysSQL); err != nil {
-		return
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.bulkSelectServerKeysStmt, bulkSelectServerSigningKeysSQL},
+		{&s.upsertServerKeysStmt, upsertServerSigningKeysSQL},
+	}.Prepare(db)
 }
 
 func (s *serverSigningKeyStatements) BulkSelectServerKeys(
diff --git a/federationapi/storage/postgres/storage.go b/federationapi/storage/postgres/storage.go
index b81f128e..468567cf 100644
--- a/federationapi/storage/postgres/storage.go
+++ b/federationapi/storage/postgres/storage.go
@@ -16,6 +16,7 @@
 package postgres
 
 import (
+	"context"
 	"database/sql"
 	"fmt"
 
@@ -23,7 +24,6 @@ import (
 	"github.com/matrix-org/dendrite/federationapi/storage/shared"
 	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
@@ -36,10 +36,10 @@ type Database struct {
 }
 
 // NewDatabase opens a new database
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (*Database, error) {
+func NewDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (*Database, error) {
 	var d Database
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()); err != nil {
+	if d.db, d.writer, err = conMan.Connection(dbProperties); err != nil {
 		return nil, err
 	}
 	blacklist, err := NewPostgresBlacklistTable(d.db)
@@ -95,7 +95,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions,
 		Version: "federationsender: drop federationsender_rooms",
 		Up:      deltas.UpRemoveRoomsTable,
 	})
-	err = m.Up(base.Context())
+	err = m.Up(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/federationapi/storage/sqlite3/blacklist_table.go b/federationapi/storage/sqlite3/blacklist_table.go
index 2694e630..5122bff1 100644
--- a/federationapi/storage/sqlite3/blacklist_table.go
+++ b/federationapi/storage/sqlite3/blacklist_table.go
@@ -60,19 +60,12 @@ func NewSQLiteBlacklistTable(db *sql.DB) (s *blacklistStatements, err error) {
 		return
 	}
 
-	if s.insertBlacklistStmt, err = db.Prepare(insertBlacklistSQL); err != nil {
-		return
-	}
-	if s.selectBlacklistStmt, err = db.Prepare(selectBlacklistSQL); err != nil {
-		return
-	}
-	if s.deleteBlacklistStmt, err = db.Prepare(deleteBlacklistSQL); err != nil {
-		return
-	}
-	if s.deleteAllBlacklistStmt, err = db.Prepare(deleteAllBlacklistSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertBlacklistStmt, insertBlacklistSQL},
+		{&s.selectBlacklistStmt, selectBlacklistSQL},
+		{&s.deleteBlacklistStmt, deleteBlacklistSQL},
+		{&s.deleteAllBlacklistStmt, deleteAllBlacklistSQL},
+	}.Prepare(db)
 }
 
 func (s *blacklistStatements) InsertBlacklist(
diff --git a/federationapi/storage/sqlite3/joined_hosts_table.go b/federationapi/storage/sqlite3/joined_hosts_table.go
index 83112c15..2f076382 100644
--- a/federationapi/storage/sqlite3/joined_hosts_table.go
+++ b/federationapi/storage/sqlite3/joined_hosts_table.go
@@ -90,22 +90,14 @@ func NewSQLiteJoinedHostsTable(db *sql.DB) (s *joinedHostsStatements, err error)
 	if err != nil {
 		return
 	}
-	if s.insertJoinedHostsStmt, err = db.Prepare(insertJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.deleteJoinedHostsStmt, err = db.Prepare(deleteJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.deleteJoinedHostsForRoomStmt, err = s.db.Prepare(deleteJoinedHostsForRoomSQL); err != nil {
-		return
-	}
-	if s.selectJoinedHostsStmt, err = db.Prepare(selectJoinedHostsSQL); err != nil {
-		return
-	}
-	if s.selectAllJoinedHostsStmt, err = db.Prepare(selectAllJoinedHostsSQL); err != nil {
-		return
-	}
-	return
+
+	return s, sqlutil.StatementList{
+		{&s.insertJoinedHostsStmt, insertJoinedHostsSQL},
+		{&s.deleteJoinedHostsStmt, deleteJoinedHostsSQL},
+		{&s.deleteJoinedHostsForRoomStmt, deleteJoinedHostsForRoomSQL},
+		{&s.selectJoinedHostsStmt, selectJoinedHostsSQL},
+		{&s.selectAllJoinedHostsStmt, selectAllJoinedHostsSQL},
+	}.Prepare(db)
 }
 
 func (s *joinedHostsStatements) InsertJoinedHosts(
diff --git a/federationapi/storage/sqlite3/notary_server_keys_json_table.go b/federationapi/storage/sqlite3/notary_server_keys_json_table.go
index 4a028fa2..24875569 100644
--- a/federationapi/storage/sqlite3/notary_server_keys_json_table.go
+++ b/federationapi/storage/sqlite3/notary_server_keys_json_table.go
@@ -19,6 +19,7 @@ import (
 	"database/sql"
 
 	"github.com/matrix-org/dendrite/federationapi/storage/tables"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
@@ -49,10 +50,9 @@ func NewSQLiteNotaryServerKeysTable(db *sql.DB) (s *notaryServerKeysStatements,
 		return
 	}
 
-	if s.insertServerKeysJSONStmt, err = db.Prepare(insertServerKeysJSONSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertServerKeysJSONStmt, insertServerKeysJSONSQL},
+	}.Prepare(db)
 }
 
 func (s *notaryServerKeysStatements) InsertJSONResponse(
diff --git a/federationapi/storage/sqlite3/notary_server_keys_metadata_table.go b/federationapi/storage/sqlite3/notary_server_keys_metadata_table.go
index 55709a96..7179eb8d 100644
--- a/federationapi/storage/sqlite3/notary_server_keys_metadata_table.go
+++ b/federationapi/storage/sqlite3/notary_server_keys_metadata_table.go
@@ -92,19 +92,12 @@ func NewSQLiteNotaryServerKeysMetadataTable(db *sql.DB) (s *notaryServerKeysMeta
 		return
 	}
 
-	if s.upsertServerKeysStmt, err = db.Prepare(upsertServerKeysSQL); err != nil {
-		return
-	}
-	if s.selectNotaryKeyResponsesStmt, err = db.Prepare(selectNotaryKeyResponsesSQL); err != nil {
-		return
-	}
-	if s.selectNotaryKeyMetadataStmt, err = db.Prepare(selectNotaryKeyMetadataSQL); err != nil {
-		return
-	}
-	if s.deleteUnusedServerKeysJSONStmt, err = db.Prepare(deleteUnusedServerKeysJSONSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.upsertServerKeysStmt, upsertServerKeysSQL},
+		{&s.selectNotaryKeyResponsesStmt, selectNotaryKeyResponsesSQL},
+		{&s.selectNotaryKeyMetadataStmt, selectNotaryKeyMetadataSQL},
+		{&s.deleteUnusedServerKeysJSONStmt, deleteUnusedServerKeysJSONSQL},
+	}.Prepare(db)
 }
 
 func (s *notaryServerKeysMetadataStatements) UpsertKey(
diff --git a/federationapi/storage/sqlite3/queue_json_table.go b/federationapi/storage/sqlite3/queue_json_table.go
index fe5896c7..0e2806d5 100644
--- a/federationapi/storage/sqlite3/queue_json_table.go
+++ b/federationapi/storage/sqlite3/queue_json_table.go
@@ -66,10 +66,10 @@ func NewSQLiteQueueJSONTable(db *sql.DB) (s *queueJSONStatements, err error) {
 	if err != nil {
 		return
 	}
-	if s.insertJSONStmt, err = db.Prepare(insertJSONSQL); err != nil {
-		return
-	}
-	return
+
+	return s, sqlutil.StatementList{
+		{&s.insertJSONStmt, insertJSONSQL},
+	}.Prepare(db)
 }
 
 func (s *queueJSONStatements) InsertQueueJSON(
diff --git a/federationapi/storage/sqlite3/queue_pdus_table.go b/federationapi/storage/sqlite3/queue_pdus_table.go
index aee8b03d..d8d99f0c 100644
--- a/federationapi/storage/sqlite3/queue_pdus_table.go
+++ b/federationapi/storage/sqlite3/queue_pdus_table.go
@@ -87,25 +87,13 @@ func NewSQLiteQueuePDUsTable(db *sql.DB) (s *queuePDUsStatements, err error) {
 	if err != nil {
 		return
 	}
-	if s.insertQueuePDUStmt, err = db.Prepare(insertQueuePDUSQL); err != nil {
-		return
-	}
-	//if s.deleteQueuePDUsStmt, err = db.Prepare(deleteQueuePDUsSQL); err != nil {
-	//	return
-	//}
-	if s.selectQueueNextTransactionIDStmt, err = db.Prepare(selectQueueNextTransactionIDSQL); err != nil {
-		return
-	}
-	if s.selectQueuePDUsStmt, err = db.Prepare(selectQueuePDUsSQL); err != nil {
-		return
-	}
-	if s.selectQueueReferenceJSONCountStmt, err = db.Prepare(selectQueuePDUsReferenceJSONCountSQL); err != nil {
-		return
-	}
-	if s.selectQueueServerNamesStmt, err = db.Prepare(selectQueuePDUsServerNamesSQL); err != nil {
-		return
-	}
-	return
+	return s, sqlutil.StatementList{
+		{&s.insertQueuePDUStmt, insertQueuePDUSQL},
+		{&s.selectQueueNextTransactionIDStmt, selectQueueNextTransactionIDSQL},
+		{&s.selectQueuePDUsStmt, selectQueuePDUsSQL},
+		{&s.selectQueueReferenceJSONCountStmt, selectQueuePDUsReferenceJSONCountSQL},
+		{&s.selectQueueServerNamesStmt, selectQueuePDUsServerNamesSQL},
+	}.Prepare(db)
 }
 
 func (s *queuePDUsStatements) InsertQueuePDU(
diff --git a/federationapi/storage/sqlite3/server_key_table.go b/federationapi/storage/sqlite3/server_key_table.go
index 9b89649f..b32ff092 100644
--- a/federationapi/storage/sqlite3/server_key_table.go
+++ b/federationapi/storage/sqlite3/server_key_table.go
@@ -74,13 +74,10 @@ func NewSQLiteServerSigningKeysTable(db *sql.DB) (s *serverSigningKeyStatements,
 	if err != nil {
 		return
 	}
-	if s.bulkSelectServerKeysStmt, err = db.Prepare(bulkSelectServerSigningKeysSQL); err != nil {
-		return
-	}
-	if s.upsertServerKeysStmt, err = db.Prepare(upsertServerSigningKeysSQL); err != nil {
-		return
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.bulkSelectServerKeysStmt, bulkSelectServerSigningKeysSQL},
+		{&s.upsertServerKeysStmt, upsertServerSigningKeysSQL},
+	}.Prepare(db)
 }
 
 func (s *serverSigningKeyStatements) BulkSelectServerKeys(
diff --git a/federationapi/storage/sqlite3/storage.go b/federationapi/storage/sqlite3/storage.go
index 1e7e41a2..c64c9a4f 100644
--- a/federationapi/storage/sqlite3/storage.go
+++ b/federationapi/storage/sqlite3/storage.go
@@ -15,13 +15,13 @@
 package sqlite3
 
 import (
+	"context"
 	"database/sql"
 
 	"github.com/matrix-org/dendrite/federationapi/storage/shared"
 	"github.com/matrix-org/dendrite/federationapi/storage/sqlite3/deltas"
 	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
@@ -34,10 +34,10 @@ type Database struct {
 }
 
 // NewDatabase opens a new database
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (*Database, error) {
+func NewDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (*Database, error) {
 	var d Database
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()); err != nil {
+	if d.db, d.writer, err = conMan.Connection(dbProperties); err != nil {
 		return nil, err
 	}
 	blacklist, err := NewSQLiteBlacklistTable(d.db)
@@ -93,7 +93,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions,
 		Version: "federationsender: drop federationsender_rooms",
 		Up:      deltas.UpRemoveRoomsTable,
 	})
-	err = m.Up(base.Context())
+	err = m.Up(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/federationapi/storage/storage.go b/federationapi/storage/storage.go
index 142e281e..4eb9d2c9 100644
--- a/federationapi/storage/storage.go
+++ b/federationapi/storage/storage.go
@@ -18,23 +18,24 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/matrix-org/dendrite/federationapi/storage/postgres"
 	"github.com/matrix-org/dendrite/federationapi/storage/sqlite3"
 	"github.com/matrix-org/dendrite/internal/caching"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
 // NewDatabase opens a new database
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (Database, error) {
+func NewDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties, cache, isLocalServerName)
+		return sqlite3.NewDatabase(ctx, conMan, dbProperties, cache, isLocalServerName)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.NewDatabase(base, dbProperties, cache, isLocalServerName)
+		return postgres.NewDatabase(ctx, conMan, dbProperties, cache, isLocalServerName)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
diff --git a/federationapi/storage/storage_wasm.go b/federationapi/storage/storage_wasm.go
index 84d5a3a4..d1652d71 100644
--- a/federationapi/storage/storage_wasm.go
+++ b/federationapi/storage/storage_wasm.go
@@ -15,20 +15,21 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/matrix-org/dendrite/federationapi/storage/sqlite3"
 	"github.com/matrix-org/dendrite/internal/caching"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
 // NewDatabase opens a new database
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (Database, error) {
+func NewDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.FederationCache, isLocalServerName func(gomatrixserverlib.ServerName) bool) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties, cache, serverName)
+		return sqlite3.NewDatabase(ctx, conMan, dbProperties, cache, isLocalServerName)
 	case dbProperties.ConnectionString.IsPostgres():
 		return nil, fmt.Errorf("can't use Postgres implementation")
 	default:
diff --git a/go.mod b/go.mod
index 780867a1..7ac294f2 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e
 	github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91
 	github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530
-	github.com/matrix-org/gomatrixserverlib v0.0.0-20230131183213-122f1e0e3fa1
+	github.com/matrix-org/gomatrixserverlib v0.0.0-20230320105331-4dd7ff2f0e3a
 	github.com/matrix-org/pinecone v0.11.1-0.20230210171230-8c3b24f2649a
 	github.com/matrix-org/util v0.0.0-20221111132719-399730281e66
 	github.com/mattn/go-sqlite3 v1.14.16
@@ -30,7 +30,6 @@ require (
 	github.com/nats-io/nats.go v1.24.0
 	github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
-	github.com/ngrok/sqlmw v0.0.0-20220520173518-97c9c04efc79
 	github.com/opentracing/opentracing-go v1.2.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index 1419b26e..bf8fbdf3 100644
--- a/go.sum
+++ b/go.sum
@@ -321,8 +321,8 @@ github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 h1:s7fexw
 github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo=
 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U=
 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
-github.com/matrix-org/gomatrixserverlib v0.0.0-20230131183213-122f1e0e3fa1 h1:JSw0nmjMrgBmoM2aQsa78LTpI5BnuD9+vOiEQ4Qo0qw=
-github.com/matrix-org/gomatrixserverlib v0.0.0-20230131183213-122f1e0e3fa1/go.mod h1:Mtifyr8q8htcBeugvlDnkBcNUy5LO8OzUoplAf1+mb4=
+github.com/matrix-org/gomatrixserverlib v0.0.0-20230320105331-4dd7ff2f0e3a h1:F6K1i61KcJ8cX/y0Q8/44Dh1w+fpESQd92gq885FDrI=
+github.com/matrix-org/gomatrixserverlib v0.0.0-20230320105331-4dd7ff2f0e3a/go.mod h1:7HTbSZe+CIdmeqVyFMekwD5dFU8khWQyngKATvd12FU=
 github.com/matrix-org/pinecone v0.11.1-0.20230210171230-8c3b24f2649a h1:awrPDf9LEFySxTLKYBMCiObelNx/cBuv/wzllvCCH3A=
 github.com/matrix-org/pinecone v0.11.1-0.20230210171230-8c3b24f2649a/go.mod h1:HchJX9oKMXaT2xYFs0Ha/6Zs06mxLU8k6F1ODnrGkeQ=
 github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y=
@@ -368,8 +368,6 @@ github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9 h1:lrVQzBtkeQE
 github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9/go.mod h1:NPHGhPc0/wudcaCqL/H5AOddkRf8GPRhzOujuUKGQu8=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
-github.com/ngrok/sqlmw v0.0.0-20220520173518-97c9c04efc79 h1:Dmx8g2747UTVPzSkmohk84S3g/uWqd6+f4SSLPhLcfA=
-github.com/ngrok/sqlmw v0.0.0-20220520173518-97c9c04efc79/go.mod h1:E26fwEtRNigBfFfHDWsklmo0T7Ixbg0XXgck+Hq4O9k=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/onsi/ginkgo/v2 v2.3.0 h1:kUMoxMoQG3ogk/QWyKh3zibV7BKZ+xBpWil1cTylVqc=
 github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
diff --git a/internal/caching/impl_ristretto.go b/internal/caching/impl_ristretto.go
index 106b9c99..7663ddcb 100644
--- a/internal/caching/impl_ristretto.go
+++ b/internal/caching/impl_ristretto.go
@@ -46,6 +46,11 @@ const (
 	eventStateKeyNIDCache
 )
 
+const (
+	DisableMetrics = false
+	EnableMetrics  = true
+)
+
 func NewRistrettoCache(maxCost config.DataUnit, maxAge time.Duration, enablePrometheus bool) *Caches {
 	cache, err := ristretto.NewCache(&ristretto.Config{
 		NumCounters: int64((maxCost / 1024) * 10), // 10 counters per 1KB data, affects bloom filter size
diff --git a/internal/fulltext/bleve.go b/internal/fulltext/bleve.go
index 7187861d..dea7c504 100644
--- a/internal/fulltext/bleve.go
+++ b/internal/fulltext/bleve.go
@@ -21,6 +21,7 @@ import (
 	"strings"
 
 	"github.com/blevesearch/bleve/v2"
+	"github.com/matrix-org/dendrite/setup/process"
 
 	// side effect imports to allow all possible languages
 	_ "github.com/blevesearch/bleve/v2/analysis/lang/ar"
@@ -55,6 +56,13 @@ type Search struct {
 	FulltextIndex bleve.Index
 }
 
+type Indexer interface {
+	Index(elements ...IndexElement) error
+	Delete(eventID string) error
+	Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (*bleve.SearchResult, error)
+	Close() error
+}
+
 // IndexElement describes the layout of an element to index
 type IndexElement struct {
 	EventID        string
@@ -77,12 +85,19 @@ func (i *IndexElement) SetContentType(v string) {
 }
 
 // New opens a new/existing fulltext index
-func New(cfg config.Fulltext) (fts *Search, err error) {
+func New(processCtx *process.ProcessContext, cfg config.Fulltext) (fts *Search, err error) {
 	fts = &Search{}
 	fts.FulltextIndex, err = openIndex(cfg)
 	if err != nil {
 		return nil, err
 	}
+	go func() {
+		processCtx.ComponentStarted()
+		// Wait for the processContext to be done, indicating that Dendrite is shutting down.
+		<-processCtx.WaitForShutdown()
+		_ = fts.Close()
+		processCtx.ComponentFinished()
+	}()
 	return fts, nil
 }
 
diff --git a/internal/fulltext/bleve_wasm.go b/internal/fulltext/bleve_wasm.go
index a69a8926..0053ed8c 100644
--- a/internal/fulltext/bleve_wasm.go
+++ b/internal/fulltext/bleve_wasm.go
@@ -15,8 +15,9 @@
 package fulltext
 
 import (
-	"github.com/matrix-org/dendrite/setup/config"
 	"time"
+
+	"github.com/matrix-org/dendrite/setup/config"
 )
 
 type Search struct{}
@@ -28,6 +29,13 @@ type IndexElement struct {
 	StreamPosition int64
 }
 
+type Indexer interface {
+	Index(elements ...IndexElement) error
+	Delete(eventID string) error
+	Search(term string, roomIDs, keys []string, limit, from int, orderByStreamPos bool) (SearchResult, error)
+	Close() error
+}
+
 type SearchResult struct {
 	Status   interface{}   `json:"status"`
 	Request  *interface{}  `json:"request"`
@@ -48,7 +56,7 @@ func (f *Search) Close() error {
 	return nil
 }
 
-func (f *Search) Index(e IndexElement) error {
+func (f *Search) Index(e ...IndexElement) error {
 	return nil
 }
 
diff --git a/internal/httputil/routing.go b/internal/httputil/routing.go
index 0bd3655e..c733c8ce 100644
--- a/internal/httputil/routing.go
+++ b/internal/httputil/routing.go
@@ -15,7 +15,10 @@
 package httputil
 
 import (
+	"net/http"
 	"net/url"
+
+	"github.com/gorilla/mux"
 )
 
 // URLDecodeMapValues is a function that iterates through each of the items in a
@@ -33,3 +36,52 @@ func URLDecodeMapValues(vmap map[string]string) (map[string]string, error) {
 
 	return decoded, nil
 }
+
+type Routers struct {
+	Client        *mux.Router
+	Federation    *mux.Router
+	Keys          *mux.Router
+	Media         *mux.Router
+	WellKnown     *mux.Router
+	Static        *mux.Router
+	DendriteAdmin *mux.Router
+	SynapseAdmin  *mux.Router
+}
+
+func NewRouters() Routers {
+	r := Routers{
+		Client:        mux.NewRouter().SkipClean(true).PathPrefix(PublicClientPathPrefix).Subrouter().UseEncodedPath(),
+		Federation:    mux.NewRouter().SkipClean(true).PathPrefix(PublicFederationPathPrefix).Subrouter().UseEncodedPath(),
+		Keys:          mux.NewRouter().SkipClean(true).PathPrefix(PublicKeyPathPrefix).Subrouter().UseEncodedPath(),
+		Media:         mux.NewRouter().SkipClean(true).PathPrefix(PublicMediaPathPrefix).Subrouter().UseEncodedPath(),
+		WellKnown:     mux.NewRouter().SkipClean(true).PathPrefix(PublicWellKnownPrefix).Subrouter().UseEncodedPath(),
+		Static:        mux.NewRouter().SkipClean(true).PathPrefix(PublicStaticPath).Subrouter().UseEncodedPath(),
+		DendriteAdmin: mux.NewRouter().SkipClean(true).PathPrefix(DendriteAdminPathPrefix).Subrouter().UseEncodedPath(),
+		SynapseAdmin:  mux.NewRouter().SkipClean(true).PathPrefix(SynapseAdminPathPrefix).Subrouter().UseEncodedPath(),
+	}
+	r.configureHTTPErrors()
+	return r
+}
+
+var NotAllowedHandler = WrapHandlerInCORS(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusMethodNotAllowed)
+	w.Header().Set("Content-Type", "application/json")
+	_, _ = w.Write([]byte(`{"errcode":"M_UNRECOGNIZED","error":"Unrecognized request"}`)) // nolint:misspell
+}))
+
+var NotFoundCORSHandler = WrapHandlerInCORS(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusNotFound)
+	w.Header().Set("Content-Type", "application/json")
+	_, _ = w.Write([]byte(`{"errcode":"M_UNRECOGNIZED","error":"Unrecognized request"}`)) // nolint:misspell
+}))
+
+func (r *Routers) configureHTTPErrors() {
+	for _, router := range []*mux.Router{
+		r.Client, r.Federation, r.Keys,
+		r.Media, r.WellKnown, r.Static,
+		r.DendriteAdmin, r.SynapseAdmin,
+	} {
+		router.NotFoundHandler = NotFoundCORSHandler
+		router.MethodNotAllowedHandler = NotAllowedHandler
+	}
+}
diff --git a/internal/sqlutil/connection_manager.go b/internal/sqlutil/connection_manager.go
new file mode 100644
index 00000000..934a2954
--- /dev/null
+++ b/internal/sqlutil/connection_manager.go
@@ -0,0 +1,75 @@
+// Copyright 2023 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sqlutil
+
+import (
+	"database/sql"
+	"fmt"
+
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
+)
+
+type Connections struct {
+	db             *sql.DB
+	writer         Writer
+	globalConfig   config.DatabaseOptions
+	processContext *process.ProcessContext
+}
+
+func NewConnectionManager(processCtx *process.ProcessContext, globalConfig config.DatabaseOptions) Connections {
+	return Connections{
+		globalConfig:   globalConfig,
+		processContext: processCtx,
+	}
+}
+
+func (c *Connections) Connection(dbProperties *config.DatabaseOptions) (*sql.DB, Writer, error) {
+	writer := NewDummyWriter()
+	if dbProperties.ConnectionString.IsSQLite() {
+		writer = NewExclusiveWriter()
+	}
+	var err error
+	if dbProperties.ConnectionString == "" {
+		// if no connectionString was provided, try the global one
+		dbProperties = &c.globalConfig
+	}
+	if dbProperties.ConnectionString != "" || c.db == nil {
+		// Open a new database connection using the supplied config.
+		c.db, err = Open(dbProperties, writer)
+		if err != nil {
+			return nil, nil, err
+		}
+		c.writer = writer
+		go func() {
+			if c.processContext == nil {
+				return
+			}
+			// If we have a ProcessContext, start a component and wait for
+			// Dendrite to shut down to cleanly close the database connection.
+			c.processContext.ComponentStarted()
+			<-c.processContext.WaitForShutdown()
+			_ = c.db.Close()
+			c.processContext.ComponentFinished()
+		}()
+		return c.db, c.writer, nil
+	}
+	if c.db != nil && c.writer != nil {
+		// Ignore the supplied config and return the global pool and
+		// writer.
+		return c.db, c.writer, nil
+	}
+	return nil, nil, fmt.Errorf("no database connections configured")
+}
diff --git a/internal/sqlutil/sqlite_cgo.go b/internal/sqlutil/sqlite_cgo.go
index efb743fc..2fe2396f 100644
--- a/internal/sqlutil/sqlite_cgo.go
+++ b/internal/sqlutil/sqlite_cgo.go
@@ -4,7 +4,6 @@
 package sqlutil
 
 import (
-	"github.com/mattn/go-sqlite3"
 	_ "github.com/mattn/go-sqlite3"
 )
 
@@ -13,7 +12,3 @@ const SQLITE_DRIVER_NAME = "sqlite3"
 func sqliteDSNExtension(dsn string) string {
 	return dsn
 }
-
-func sqliteDriver() *sqlite3.SQLiteDriver {
-	return &sqlite3.SQLiteDriver{}
-}
diff --git a/internal/sqlutil/sqlite_native.go b/internal/sqlutil/sqlite_native.go
index ed500afc..3c545b65 100644
--- a/internal/sqlutil/sqlite_native.go
+++ b/internal/sqlutil/sqlite_native.go
@@ -4,7 +4,6 @@
 package sqlutil
 
 import (
-	"modernc.org/sqlite"
 	"strings"
 )
 
@@ -23,7 +22,3 @@ func sqliteDSNExtension(dsn string) string {
 	dsn += "_pragma=busy_timeout%3d10000"
 	return dsn
 }
-
-func sqliteDriver() *sqlite.Driver {
-	return &sqlite.Driver{}
-}
diff --git a/internal/sqlutil/sqlutil.go b/internal/sqlutil/sqlutil.go
index 39a067e5..b2d0d218 100644
--- a/internal/sqlutil/sqlutil.go
+++ b/internal/sqlutil/sqlutil.go
@@ -13,8 +13,7 @@ import (
 var skipSanityChecks = flag.Bool("skip-db-sanity", false, "Ignore sanity checks on the database connections (NOT RECOMMENDED!)")
 
 // Open opens a database specified by its database driver name and a driver-specific data source name,
-// usually consisting of at least a database name and connection information. Includes tracing driver
-// if DENDRITE_TRACE_SQL=1
+// usually consisting of at least a database name and connection information.
 func Open(dbProperties *config.DatabaseOptions, writer Writer) (*sql.DB, error) {
 	var err error
 	var driverName, dsn string
@@ -32,10 +31,6 @@ func Open(dbProperties *config.DatabaseOptions, writer Writer) (*sql.DB, error)
 	default:
 		return nil, fmt.Errorf("invalid database connection string %q", dbProperties.ConnectionString)
 	}
-	if tracingEnabled {
-		// install the wrapped driver
-		driverName += "-trace"
-	}
 	db, err := sql.Open(driverName, dsn)
 	if err != nil {
 		return nil, err
diff --git a/internal/sqlutil/trace.go b/internal/sqlutil/trace.go
deleted file mode 100644
index 7b637106..00000000
--- a/internal/sqlutil/trace.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2020 The Matrix.org Foundation C.I.C.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package sqlutil
-
-import (
-	"context"
-	"database/sql/driver"
-	"fmt"
-	"io"
-	"os"
-	"runtime"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/ngrok/sqlmw"
-	"github.com/sirupsen/logrus"
-)
-
-var tracingEnabled = os.Getenv("DENDRITE_TRACE_SQL") == "1"
-var goidToWriter sync.Map
-
-type traceInterceptor struct {
-	sqlmw.NullInterceptor
-}
-
-func (in *traceInterceptor) StmtQueryContext(ctx context.Context, stmt driver.StmtQueryContext, query string, args []driver.NamedValue) (context.Context, driver.Rows, error) {
-	startedAt := time.Now()
-	rows, err := stmt.QueryContext(ctx, args)
-
-	trackGoID(query)
-
-	logrus.WithField("duration", time.Since(startedAt)).WithField(logrus.ErrorKey, err).Debug("executed sql query ", query, " args: ", args)
-
-	return ctx, rows, err
-}
-
-func (in *traceInterceptor) StmtExecContext(ctx context.Context, stmt driver.StmtExecContext, query string, args []driver.NamedValue) (driver.Result, error) {
-	startedAt := time.Now()
-	result, err := stmt.ExecContext(ctx, args)
-
-	trackGoID(query)
-
-	logrus.WithField("duration", time.Since(startedAt)).WithField(logrus.ErrorKey, err).Debug("executed sql query ", query, " args: ", args)
-
-	return result, err
-}
-
-func (in *traceInterceptor) RowsNext(c context.Context, rows driver.Rows, dest []driver.Value) error {
-	err := rows.Next(dest)
-	if err == io.EOF {
-		// For all cases, we call Next() n+1 times, the first to populate the initial dest, then eventually
-		// it will io.EOF. If we log on each Next() call we log the last element twice, so don't.
-		return err
-	}
-	cols := rows.Columns()
-	logrus.Debug(strings.Join(cols, " | "))
-
-	b := strings.Builder{}
-	for i, val := range dest {
-		b.WriteString(fmt.Sprintf("%q", val))
-		if i+1 <= len(dest)-1 {
-			b.WriteString(" | ")
-		}
-	}
-	logrus.Debug(b.String())
-	return err
-}
-
-func trackGoID(query string) {
-	thisGoID := goid()
-	if _, ok := goidToWriter.Load(thisGoID); ok {
-		return // we're on a writer goroutine
-	}
-
-	q := strings.TrimSpace(query)
-	if strings.HasPrefix(q, "SELECT") {
-		return // SELECTs can go on other goroutines
-	}
-	logrus.Warnf("unsafe goid %d: SQL executed not on an ExclusiveWriter: %s", thisGoID, q)
-}
-
-func init() {
-	registerDrivers()
-}
-
-func goid() int {
-	var buf [64]byte
-	n := runtime.Stack(buf[:], false)
-	idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
-	id, err := strconv.Atoi(idField)
-	if err != nil {
-		panic(fmt.Sprintf("cannot get goroutine id: %v", err))
-	}
-	return id
-}
diff --git a/internal/sqlutil/trace_driver.go b/internal/sqlutil/trace_driver.go
deleted file mode 100644
index a2e0d12e..00000000
--- a/internal/sqlutil/trace_driver.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2020 The Matrix.org Foundation C.I.C.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//go:build !wasm
-// +build !wasm
-
-package sqlutil
-
-import (
-	"database/sql"
-
-	"github.com/lib/pq"
-	"github.com/ngrok/sqlmw"
-)
-
-func registerDrivers() {
-	if !tracingEnabled {
-		return
-	}
-	// install the wrapped drivers
-	sql.Register("postgres-trace", sqlmw.Driver(&pq.Driver{}, new(traceInterceptor)))
-	sql.Register("sqlite3-trace", sqlmw.Driver(sqliteDriver(), new(traceInterceptor)))
-
-}
diff --git a/internal/sqlutil/trace_driver_wasm.go b/internal/sqlutil/trace_driver_wasm.go
deleted file mode 100644
index 51b60c3c..00000000
--- a/internal/sqlutil/trace_driver_wasm.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 The Matrix.org Foundation C.I.C.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//go:build wasm
-// +build wasm
-
-package sqlutil
-
-import (
-	"database/sql"
-
-	sqlitejs "github.com/matrix-org/go-sqlite3-js"
-	"github.com/ngrok/sqlmw"
-)
-
-func registerDrivers() {
-	if !tracingEnabled {
-		return
-	}
-	// install the wrapped drivers
-	sql.Register("sqlite3_js-trace", sqlmw.Driver(&sqlitejs.SqliteJsDriver{}, new(traceInterceptor)))
-
-}
diff --git a/internal/sqlutil/writer_exclusive.go b/internal/sqlutil/writer_exclusive.go
index 8eff3ce5..c6a271c1 100644
--- a/internal/sqlutil/writer_exclusive.go
+++ b/internal/sqlutil/writer_exclusive.go
@@ -60,11 +60,6 @@ func (w *ExclusiveWriter) run() {
 	if !w.running.CompareAndSwap(false, true) {
 		return
 	}
-	if tracingEnabled {
-		gid := goid()
-		goidToWriter.Store(gid, w)
-		defer goidToWriter.Delete(gid)
-	}
 
 	defer w.running.Store(false)
 	for task := range w.todo {
diff --git a/mediaapi/mediaapi.go b/mediaapi/mediaapi.go
index 4792c996..5d517ef2 100644
--- a/mediaapi/mediaapi.go
+++ b/mediaapi/mediaapi.go
@@ -15,9 +15,11 @@
 package mediaapi
 
 import (
+	"github.com/gorilla/mux"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/routing"
 	"github.com/matrix-org/dendrite/mediaapi/storage"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/sirupsen/logrus"
@@ -25,19 +27,18 @@ import (
 
 // AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component.
 func AddPublicRoutes(
-	base *base.BaseDendrite,
+	mediaRouter *mux.Router,
+	cm sqlutil.Connections,
+	cfg *config.Dendrite,
 	userAPI userapi.MediaUserAPI,
 	client *gomatrixserverlib.Client,
 ) {
-	cfg := &base.Cfg.MediaAPI
-	rateCfg := &base.Cfg.ClientAPI.RateLimiting
-
-	mediaDB, err := storage.NewMediaAPIDatasource(base, &cfg.Database)
+	mediaDB, err := storage.NewMediaAPIDatasource(cm, &cfg.MediaAPI.Database)
 	if err != nil {
 		logrus.WithError(err).Panicf("failed to connect to media db")
 	}
 
 	routing.Setup(
-		base.PublicMediaAPIMux, cfg, rateCfg, mediaDB, userAPI, client,
+		mediaRouter, cfg, mediaDB, userAPI, client,
 	)
 }
diff --git a/mediaapi/routing/routing.go b/mediaapi/routing/routing.go
index 50af2f88..98e6a82e 100644
--- a/mediaapi/routing/routing.go
+++ b/mediaapi/routing/routing.go
@@ -35,7 +35,7 @@ import (
 // configResponse is the response to GET /_matrix/media/r0/config
 // https://matrix.org/docs/spec/client_server/latest#get-matrix-media-r0-config
 type configResponse struct {
-	UploadSize *config.FileSizeBytes `json:"m.upload.size"`
+	UploadSize *config.FileSizeBytes `json:"m.upload.size,omitempty"`
 }
 
 // Setup registers the media API HTTP handlers
@@ -45,13 +45,12 @@ type configResponse struct {
 // nolint: gocyclo
 func Setup(
 	publicAPIMux *mux.Router,
-	cfg *config.MediaAPI,
-	rateLimit *config.RateLimiting,
+	cfg *config.Dendrite,
 	db storage.Database,
 	userAPI userapi.MediaUserAPI,
 	client *gomatrixserverlib.Client,
 ) {
-	rateLimits := httputil.NewRateLimits(rateLimit)
+	rateLimits := httputil.NewRateLimits(&cfg.ClientAPI.RateLimiting)
 
 	v3mux := publicAPIMux.PathPrefix("/{apiversion:(?:r0|v1|v3)}/").Subrouter()
 
@@ -65,7 +64,7 @@ func Setup(
 			if r := rateLimits.Limit(req, dev); r != nil {
 				return *r
 			}
-			return Upload(req, cfg, dev, db, activeThumbnailGeneration)
+			return Upload(req, &cfg.MediaAPI, dev, db, activeThumbnailGeneration)
 		},
 	)
 
@@ -73,8 +72,8 @@ func Setup(
 		if r := rateLimits.Limit(req, device); r != nil {
 			return *r
 		}
-		respondSize := &cfg.MaxFileSizeBytes
-		if cfg.MaxFileSizeBytes == 0 {
+		respondSize := &cfg.MediaAPI.MaxFileSizeBytes
+		if cfg.MediaAPI.MaxFileSizeBytes == 0 {
 			respondSize = nil
 		}
 		return util.JSONResponse{
@@ -90,12 +89,12 @@ func Setup(
 		MXCToResult: map[string]*types.RemoteRequestResult{},
 	}
 
-	downloadHandler := makeDownloadAPI("download", cfg, rateLimits, db, client, activeRemoteRequests, activeThumbnailGeneration)
+	downloadHandler := makeDownloadAPI("download", &cfg.MediaAPI, rateLimits, db, client, activeRemoteRequests, activeThumbnailGeneration)
 	v3mux.Handle("/download/{serverName}/{mediaId}", downloadHandler).Methods(http.MethodGet, http.MethodOptions)
 	v3mux.Handle("/download/{serverName}/{mediaId}/{downloadName}", downloadHandler).Methods(http.MethodGet, http.MethodOptions)
 
 	v3mux.Handle("/thumbnail/{serverName}/{mediaId}",
-		makeDownloadAPI("thumbnail", cfg, rateLimits, db, client, activeRemoteRequests, activeThumbnailGeneration),
+		makeDownloadAPI("thumbnail", &cfg.MediaAPI, rateLimits, db, client, activeRemoteRequests, activeThumbnailGeneration),
 	).Methods(http.MethodGet, http.MethodOptions)
 }
 
diff --git a/mediaapi/storage/postgres/mediaapi.go b/mediaapi/storage/postgres/mediaapi.go
index 30ec64f8..5b668774 100644
--- a/mediaapi/storage/postgres/mediaapi.go
+++ b/mediaapi/storage/postgres/mediaapi.go
@@ -20,13 +20,12 @@ import (
 	_ "github.com/lib/pq"
 	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/storage/shared"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
 // NewDatabase opens a postgres database.
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.Database, error) {
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter())
+func NewDatabase(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (*shared.Database, error) {
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, err
 	}
diff --git a/mediaapi/storage/sqlite3/mediaapi.go b/mediaapi/storage/sqlite3/mediaapi.go
index c0ab10e9..4d484f32 100644
--- a/mediaapi/storage/sqlite3/mediaapi.go
+++ b/mediaapi/storage/sqlite3/mediaapi.go
@@ -19,13 +19,12 @@ import (
 	// Import the postgres database driver.
 	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/storage/shared"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
 // NewDatabase opens a SQLIte database.
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.Database, error) {
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter())
+func NewDatabase(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (*shared.Database, error) {
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, err
 	}
diff --git a/mediaapi/storage/storage.go b/mediaapi/storage/storage.go
index f673ae7e..8e67af9f 100644
--- a/mediaapi/storage/storage.go
+++ b/mediaapi/storage/storage.go
@@ -20,19 +20,19 @@ package storage
 import (
 	"fmt"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/storage/postgres"
 	"github.com/matrix-org/dendrite/mediaapi/storage/sqlite3"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
 // NewMediaAPIDatasource opens a database connection.
-func NewMediaAPIDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) {
+func NewMediaAPIDatasource(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties)
+		return sqlite3.NewDatabase(conMan, dbProperties)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.NewDatabase(base, dbProperties)
+		return postgres.NewDatabase(conMan, dbProperties)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
diff --git a/mediaapi/storage/storage_wasm.go b/mediaapi/storage/storage_wasm.go
index 41e4a28c..47ee3792 100644
--- a/mediaapi/storage/storage_wasm.go
+++ b/mediaapi/storage/storage_wasm.go
@@ -17,16 +17,16 @@ package storage
 import (
 	"fmt"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/storage/sqlite3"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
 // Open opens a postgres database.
-func NewMediaAPIDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) {
+func NewMediaAPIDatasource(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties)
+		return sqlite3.NewDatabase(conMan, dbProperties)
 	case dbProperties.ConnectionString.IsPostgres():
 		return nil, fmt.Errorf("can't use Postgres implementation")
 	default:
diff --git a/relayapi/relayapi.go b/relayapi/relayapi.go
index 200a1814..fdc366e4 100644
--- a/relayapi/relayapi.go
+++ b/relayapi/relayapi.go
@@ -16,24 +16,26 @@ package relayapi
 
 import (
 	"github.com/matrix-org/dendrite/federationapi/producers"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/relayapi/api"
 	"github.com/matrix-org/dendrite/relayapi/internal"
 	"github.com/matrix-org/dendrite/relayapi/routing"
 	"github.com/matrix-org/dendrite/relayapi/storage"
 	rsAPI "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/sirupsen/logrus"
 )
 
 // AddPublicRoutes sets up and registers HTTP handlers on the base API muxes for the FederationAPI component.
 func AddPublicRoutes(
-	base *base.BaseDendrite,
+	routers httputil.Routers,
+	dendriteCfg *config.Dendrite,
 	keyRing gomatrixserverlib.JSONVerifier,
 	relayAPI api.RelayInternalAPI,
 ) {
-	fedCfg := &base.Cfg.FederationAPI
-
 	relay, ok := relayAPI.(*internal.RelayInternalAPI)
 	if !ok {
 		panic("relayapi.AddPublicRoutes called with a RelayInternalAPI impl which was not " +
@@ -41,24 +43,24 @@ func AddPublicRoutes(
 	}
 
 	routing.Setup(
-		base.PublicFederationAPIMux,
-		fedCfg,
+		routers.Federation,
+		&dendriteCfg.FederationAPI,
 		relay,
 		keyRing,
 	)
 }
 
 func NewRelayInternalAPI(
-	base *base.BaseDendrite,
+	dendriteCfg *config.Dendrite,
+	cm sqlutil.Connections,
 	fedClient *gomatrixserverlib.FederationClient,
 	rsAPI rsAPI.RoomserverInternalAPI,
 	keyRing *gomatrixserverlib.KeyRing,
 	producer *producers.SyncAPIProducer,
 	relayingEnabled bool,
+	caches caching.FederationCache,
 ) api.RelayInternalAPI {
-	cfg := &base.Cfg.RelayAPI
-
-	relayDB, err := storage.NewDatabase(base, &cfg.Database, base.Caches, base.Cfg.Global.IsLocalServerName)
+	relayDB, err := storage.NewDatabase(cm, &dendriteCfg.RelayAPI.Database, caches, dendriteCfg.Global.IsLocalServerName)
 	if err != nil {
 		logrus.WithError(err).Panic("failed to connect to relay db")
 	}
@@ -69,8 +71,8 @@ func NewRelayInternalAPI(
 		rsAPI,
 		keyRing,
 		producer,
-		base.Cfg.Global.Presence.EnableInbound,
-		base.Cfg.Global.ServerName,
+		dendriteCfg.Global.Presence.EnableInbound,
+		dendriteCfg.Global.ServerName,
 		relayingEnabled,
 	)
 }
diff --git a/relayapi/storage/postgres/storage.go b/relayapi/storage/postgres/storage.go
index 1042beba..78bbaf1c 100644
--- a/relayapi/storage/postgres/storage.go
+++ b/relayapi/storage/postgres/storage.go
@@ -20,7 +20,6 @@ import (
 	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/relayapi/storage/shared"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
@@ -34,14 +33,14 @@ type Database struct {
 
 // NewDatabase opens a new database
 func NewDatabase(
-	base *base.BaseDendrite,
+	conMan sqlutil.Connections,
 	dbProperties *config.DatabaseOptions,
 	cache caching.FederationCache,
 	isLocalServerName func(gomatrixserverlib.ServerName) bool,
 ) (*Database, error) {
 	var d Database
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()); err != nil {
+	if d.db, d.writer, err = conMan.Connection(dbProperties); err != nil {
 		return nil, err
 	}
 	queue, err := NewPostgresRelayQueueTable(d.db)
diff --git a/relayapi/storage/sqlite3/storage.go b/relayapi/storage/sqlite3/storage.go
index 3ed4ab04..da2cf9f7 100644
--- a/relayapi/storage/sqlite3/storage.go
+++ b/relayapi/storage/sqlite3/storage.go
@@ -20,7 +20,6 @@ import (
 	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/relayapi/storage/shared"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
@@ -34,14 +33,14 @@ type Database struct {
 
 // NewDatabase opens a new database
 func NewDatabase(
-	base *base.BaseDendrite,
+	conMan sqlutil.Connections,
 	dbProperties *config.DatabaseOptions,
 	cache caching.FederationCache,
 	isLocalServerName func(gomatrixserverlib.ServerName) bool,
 ) (*Database, error) {
 	var d Database
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()); err != nil {
+	if d.db, d.writer, err = conMan.Connection(dbProperties); err != nil {
 		return nil, err
 	}
 	queue, err := NewSQLiteRelayQueueTable(d.db)
diff --git a/relayapi/storage/storage.go b/relayapi/storage/storage.go
index 16ecbcfb..17d9e6c7 100644
--- a/relayapi/storage/storage.go
+++ b/relayapi/storage/storage.go
@@ -21,25 +21,25 @@ import (
 	"fmt"
 
 	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/relayapi/storage/postgres"
 	"github.com/matrix-org/dendrite/relayapi/storage/sqlite3"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
 // NewDatabase opens a new database
 func NewDatabase(
-	base *base.BaseDendrite,
+	conMan sqlutil.Connections,
 	dbProperties *config.DatabaseOptions,
 	cache caching.FederationCache,
 	isLocalServerName func(gomatrixserverlib.ServerName) bool,
 ) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties, cache, isLocalServerName)
+		return sqlite3.NewDatabase(conMan, dbProperties, cache, isLocalServerName)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.NewDatabase(base, dbProperties, cache, isLocalServerName)
+		return postgres.NewDatabase(conMan, dbProperties, cache, isLocalServerName)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
diff --git a/relayapi/storage/storage_wasm.go b/relayapi/storage/storage_wasm.go
new file mode 100644
index 00000000..92847e1b
--- /dev/null
+++ b/relayapi/storage/storage_wasm.go
@@ -0,0 +1,42 @@
+// Copyright 2022 The Matrix.org Foundation C.I.C.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package storage
+
+import (
+	"fmt"
+
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/relayapi/storage/sqlite3"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/gomatrixserverlib"
+)
+
+// NewDatabase opens a new database
+func NewDatabase(
+	conMan sqlutil.Connections,
+	dbProperties *config.DatabaseOptions,
+	cache caching.FederationCache,
+	isLocalServerName func(gomatrixserverlib.ServerName) bool,
+) (Database, error) {
+	switch {
+	case dbProperties.ConnectionString.IsSQLite():
+		return sqlite3.NewDatabase(conMan, dbProperties, cache, isLocalServerName)
+	case dbProperties.ConnectionString.IsPostgres():
+		return nil, fmt.Errorf("can't use Postgres implementation")
+	default:
+		return nil, fmt.Errorf("unexpected database type")
+	}
+}
diff --git a/roomserver/internal/alias.go b/roomserver/internal/alias.go
index fc61b7f4..94b8b16c 100644
--- a/roomserver/internal/alias.go
+++ b/roomserver/internal/alias.go
@@ -117,7 +117,7 @@ func (r *RoomserverInternalAPI) RemoveRoomAlias(
 	request *api.RemoveRoomAliasRequest,
 	response *api.RemoveRoomAliasResponse,
 ) error {
-	_, virtualHost, err := r.Cfg.Matrix.SplitLocalID('@', request.UserID)
+	_, virtualHost, err := r.Cfg.Global.SplitLocalID('@', request.UserID)
 	if err != nil {
 		return err
 	}
@@ -175,12 +175,12 @@ func (r *RoomserverInternalAPI) RemoveRoomAlias(
 				sender = ev.Sender()
 			}
 
-			_, senderDomain, err := r.Cfg.Matrix.SplitLocalID('@', sender)
+			_, senderDomain, err := r.Cfg.Global.SplitLocalID('@', sender)
 			if err != nil {
 				return err
 			}
 
-			identity, err := r.Cfg.Matrix.SigningIdentityFor(senderDomain)
+			identity, err := r.Cfg.Global.SigningIdentityFor(senderDomain)
 			if err != nil {
 				return err
 			}
@@ -206,7 +206,7 @@ func (r *RoomserverInternalAPI) RemoveRoomAlias(
 				return err
 			}
 
-			newEvent, err := eventutil.BuildEvent(ctx, builder, r.Cfg.Matrix, identity, time.Now(), &eventsNeeded, stateRes)
+			newEvent, err := eventutil.BuildEvent(ctx, builder, &r.Cfg.Global, identity, time.Now(), &eventsNeeded, stateRes)
 			if err != nil {
 				return err
 			}
diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go
index c43b9d04..7ca3675d 100644
--- a/roomserver/internal/api.go
+++ b/roomserver/internal/api.go
@@ -18,7 +18,6 @@ import (
 	"github.com/matrix-org/dendrite/roomserver/internal/query"
 	"github.com/matrix-org/dendrite/roomserver/producers"
 	"github.com/matrix-org/dendrite/roomserver/storage"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/setup/process"
@@ -41,9 +40,8 @@ type RoomserverInternalAPI struct {
 	*perform.Upgrader
 	*perform.Admin
 	ProcessContext         *process.ProcessContext
-	Base                   *base.BaseDendrite
 	DB                     storage.Database
-	Cfg                    *config.RoomServer
+	Cfg                    *config.Dendrite
 	Cache                  caching.RoomServerCaches
 	ServerName             gomatrixserverlib.ServerName
 	KeyRing                gomatrixserverlib.JSONVerifier
@@ -56,43 +54,44 @@ type RoomserverInternalAPI struct {
 	InputRoomEventTopic    string // JetStream topic for new input room events
 	OutputProducer         *producers.RoomEventProducer
 	PerspectiveServerNames []gomatrixserverlib.ServerName
+	enableMetrics          bool
 }
 
 func NewRoomserverAPI(
-	base *base.BaseDendrite, roomserverDB storage.Database,
-	js nats.JetStreamContext, nc *nats.Conn,
+	processContext *process.ProcessContext, dendriteCfg *config.Dendrite, roomserverDB storage.Database,
+	js nats.JetStreamContext, nc *nats.Conn, caches caching.RoomServerCaches, enableMetrics bool,
 ) *RoomserverInternalAPI {
 	var perspectiveServerNames []gomatrixserverlib.ServerName
-	for _, kp := range base.Cfg.FederationAPI.KeyPerspectives {
+	for _, kp := range dendriteCfg.FederationAPI.KeyPerspectives {
 		perspectiveServerNames = append(perspectiveServerNames, kp.ServerName)
 	}
 
 	serverACLs := acls.NewServerACLs(roomserverDB)
 	producer := &producers.RoomEventProducer{
-		Topic:     string(base.Cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent)),
+		Topic:     string(dendriteCfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent)),
 		JetStream: js,
 		ACLs:      serverACLs,
 	}
 	a := &RoomserverInternalAPI{
-		ProcessContext:         base.ProcessContext,
+		ProcessContext:         processContext,
 		DB:                     roomserverDB,
-		Base:                   base,
-		Cfg:                    &base.Cfg.RoomServer,
-		Cache:                  base.Caches,
-		ServerName:             base.Cfg.Global.ServerName,
+		Cfg:                    dendriteCfg,
+		Cache:                  caches,
+		ServerName:             dendriteCfg.Global.ServerName,
 		PerspectiveServerNames: perspectiveServerNames,
-		InputRoomEventTopic:    base.Cfg.Global.JetStream.Prefixed(jetstream.InputRoomEvent),
+		InputRoomEventTopic:    dendriteCfg.Global.JetStream.Prefixed(jetstream.InputRoomEvent),
 		OutputProducer:         producer,
 		JetStream:              js,
 		NATSClient:             nc,
-		Durable:                base.Cfg.Global.JetStream.Durable("RoomserverInputConsumer"),
+		Durable:                dendriteCfg.Global.JetStream.Durable("RoomserverInputConsumer"),
 		ServerACLs:             serverACLs,
 		Queryer: &query.Queryer{
 			DB:                roomserverDB,
-			Cache:             base.Caches,
-			IsLocalServerName: base.Cfg.Global.IsLocalServerName,
+			Cache:             caches,
+			IsLocalServerName: dendriteCfg.Global.IsLocalServerName,
 			ServerACLs:        serverACLs,
 		},
+		enableMetrics: enableMetrics,
 		// perform-er structs get initialised when we have a federation sender to use
 	}
 	return a
@@ -105,15 +104,14 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
 	r.fsAPI = fsAPI
 	r.KeyRing = keyRing
 
-	identity, err := r.Cfg.Matrix.SigningIdentityFor(r.ServerName)
+	identity, err := r.Cfg.Global.SigningIdentityFor(r.ServerName)
 	if err != nil {
 		logrus.Panic(err)
 	}
 
 	r.Inputer = &input.Inputer{
-		Cfg:                 &r.Base.Cfg.RoomServer,
-		Base:                r.Base,
-		ProcessContext:      r.Base.ProcessContext,
+		Cfg:                 &r.Cfg.RoomServer,
+		ProcessContext:      r.ProcessContext,
 		DB:                  r.DB,
 		InputRoomEventTopic: r.InputRoomEventTopic,
 		OutputProducer:      r.OutputProducer,
@@ -129,12 +127,12 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
 	}
 	r.Inviter = &perform.Inviter{
 		DB:      r.DB,
-		Cfg:     r.Cfg,
+		Cfg:     &r.Cfg.RoomServer,
 		FSAPI:   r.fsAPI,
 		Inputer: r.Inputer,
 	}
 	r.Joiner = &perform.Joiner{
-		Cfg:     r.Cfg,
+		Cfg:     &r.Cfg.RoomServer,
 		DB:      r.DB,
 		FSAPI:   r.fsAPI,
 		RSAPI:   r,
@@ -143,7 +141,7 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
 	}
 	r.Peeker = &perform.Peeker{
 		ServerName: r.ServerName,
-		Cfg:        r.Cfg,
+		Cfg:        &r.Cfg.RoomServer,
 		DB:         r.DB,
 		FSAPI:      r.fsAPI,
 		Inputer:    r.Inputer,
@@ -154,12 +152,12 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
 	}
 	r.Unpeeker = &perform.Unpeeker{
 		ServerName: r.ServerName,
-		Cfg:        r.Cfg,
+		Cfg:        &r.Cfg.RoomServer,
 		FSAPI:      r.fsAPI,
 		Inputer:    r.Inputer,
 	}
 	r.Leaver = &perform.Leaver{
-		Cfg:     r.Cfg,
+		Cfg:     &r.Cfg.RoomServer,
 		DB:      r.DB,
 		FSAPI:   r.fsAPI,
 		Inputer: r.Inputer,
@@ -168,7 +166,7 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
 		DB: r.DB,
 	}
 	r.Backfiller = &perform.Backfiller{
-		IsLocalServerName: r.Cfg.Matrix.IsLocalServerName,
+		IsLocalServerName: r.Cfg.Global.IsLocalServerName,
 		DB:                r.DB,
 		FSAPI:             r.fsAPI,
 		KeyRing:           r.KeyRing,
@@ -181,12 +179,12 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio
 		DB: r.DB,
 	}
 	r.Upgrader = &perform.Upgrader{
-		Cfg:    r.Cfg,
+		Cfg:    &r.Cfg.RoomServer,
 		URSAPI: r,
 	}
 	r.Admin = &perform.Admin{
 		DB:      r.DB,
-		Cfg:     r.Cfg,
+		Cfg:     &r.Cfg.RoomServer,
 		Inputer: r.Inputer,
 		Queryer: r.Queryer,
 		Leaver:  r.Leaver,
diff --git a/roomserver/internal/input/input.go b/roomserver/internal/input/input.go
index 2ec19f01..cc0c673d 100644
--- a/roomserver/internal/input/input.go
+++ b/roomserver/internal/input/input.go
@@ -39,7 +39,6 @@ import (
 	"github.com/matrix-org/dendrite/roomserver/producers"
 	"github.com/matrix-org/dendrite/roomserver/storage"
 	"github.com/matrix-org/dendrite/roomserver/types"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/setup/process"
@@ -74,7 +73,6 @@ import (
 // or C.
 type Inputer struct {
 	Cfg                 *config.RoomServer
-	Base                *base.BaseDendrite
 	ProcessContext      *process.ProcessContext
 	DB                  storage.RoomDatabase
 	NATSClient          *nats.Conn
@@ -89,8 +87,9 @@ type Inputer struct {
 	OutputProducer      *producers.RoomEventProducer
 	workers             sync.Map // room ID -> *worker
 
-	Queryer *query.Queryer
-	UserAPI userapi.RoomserverUserAPI
+	Queryer       *query.Queryer
+	UserAPI       userapi.RoomserverUserAPI
+	enableMetrics bool
 }
 
 // If a room consumer is inactive for a while then we will allow NATS
@@ -177,7 +176,7 @@ func (r *Inputer) startWorkerForRoom(roomID string) {
 // will look to see if we have a worker for that room which has its
 // own consumer. If we don't, we'll start one.
 func (r *Inputer) Start() error {
-	if r.Base.EnableMetrics {
+	if r.enableMetrics {
 		prometheus.MustRegister(roomserverInputBackpressure, processRoomEventDuration)
 	}
 	_, err := r.JetStream.Subscribe(
diff --git a/roomserver/roomserver.go b/roomserver/roomserver.go
index 5a8d8b57..4685f474 100644
--- a/roomserver/roomserver.go
+++ b/roomserver/roomserver.go
@@ -15,28 +15,35 @@
 package roomserver
 
 import (
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/sirupsen/logrus"
 
 	"github.com/matrix-org/dendrite/roomserver/api"
 	"github.com/matrix-org/dendrite/roomserver/internal"
 	"github.com/matrix-org/dendrite/roomserver/storage"
-	"github.com/matrix-org/dendrite/setup/base"
 )
 
 // NewInternalAPI returns a concrete implementation of the internal API.
 func NewInternalAPI(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	cfg *config.Dendrite,
+	cm sqlutil.Connections,
+	natsInstance *jetstream.NATSInstance,
+	caches caching.RoomServerCaches,
+	enableMetrics bool,
 ) api.RoomserverInternalAPI {
-	cfg := &base.Cfg.RoomServer
-
-	roomserverDB, err := storage.Open(base, &cfg.Database, base.Caches)
+	roomserverDB, err := storage.Open(processContext.Context(), cm, &cfg.RoomServer.Database, caches)
 	if err != nil {
 		logrus.WithError(err).Panicf("failed to connect to room server db")
 	}
 
-	js, nc := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
+	js, nc := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
 
 	return internal.NewRoomserverAPI(
-		base, roomserverDB, js, nc,
+		processContext, cfg, roomserverDB, js, nc, caches, enableMetrics,
 	)
 }
diff --git a/roomserver/storage/postgres/storage.go b/roomserver/storage/postgres/storage.go
index d98a5cf9..19cde541 100644
--- a/roomserver/storage/postgres/storage.go
+++ b/roomserver/storage/postgres/storage.go
@@ -28,7 +28,6 @@ import (
 	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver/storage/postgres/deltas"
 	"github.com/matrix-org/dendrite/roomserver/storage/shared"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
@@ -38,10 +37,10 @@ type Database struct {
 }
 
 // Open a postgres database.
-func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) {
+func Open(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) {
 	var d Database
 	var err error
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter())
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, fmt.Errorf("sqlutil.Open: %w", err)
 	}
@@ -53,7 +52,7 @@ func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache c
 
 	// Special case, since this migration uses several tables, so it needs to
 	// be sure that all tables are created first.
-	if err = executeMigration(base.Context(), db); err != nil {
+	if err = executeMigration(ctx, db); err != nil {
 		return nil, err
 	}
 
diff --git a/roomserver/storage/sqlite3/storage.go b/roomserver/storage/sqlite3/storage.go
index 2adedd2d..89e16fc1 100644
--- a/roomserver/storage/sqlite3/storage.go
+++ b/roomserver/storage/sqlite3/storage.go
@@ -28,7 +28,6 @@ import (
 	"github.com/matrix-org/dendrite/roomserver/storage/shared"
 	"github.com/matrix-org/dendrite/roomserver/storage/sqlite3/deltas"
 	"github.com/matrix-org/dendrite/roomserver/types"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
@@ -38,10 +37,10 @@ type Database struct {
 }
 
 // Open a sqlite database.
-func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) {
+func Open(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) {
 	var d Database
 	var err error
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter())
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, fmt.Errorf("sqlutil.Open: %w", err)
 	}
@@ -62,7 +61,7 @@ func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache c
 
 	// Special case, since this migration uses several tables, so it needs to
 	// be sure that all tables are created first.
-	if err = executeMigration(base.Context(), db); err != nil {
+	if err = executeMigration(ctx, db); err != nil {
 		return nil, err
 	}
 
diff --git a/roomserver/storage/storage.go b/roomserver/storage/storage.go
index 8a87b7d7..2b3b3bd8 100644
--- a/roomserver/storage/storage.go
+++ b/roomserver/storage/storage.go
@@ -18,22 +18,23 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver/storage/postgres"
 	"github.com/matrix-org/dendrite/roomserver/storage/sqlite3"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
 // Open opens a database connection.
-func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) {
+func Open(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.Open(base, dbProperties, cache)
+		return sqlite3.Open(ctx, conMan, dbProperties, cache)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.Open(base, dbProperties, cache)
+		return postgres.Open(ctx, conMan, dbProperties, cache)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
diff --git a/roomserver/storage/storage_wasm.go b/roomserver/storage/storage_wasm.go
index df5a56ac..817f9304 100644
--- a/roomserver/storage/storage_wasm.go
+++ b/roomserver/storage/storage_wasm.go
@@ -15,19 +15,20 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver/storage/sqlite3"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
 // NewPublicRoomsServerDatabase opens a database connection.
-func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) {
+func Open(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.Open(base, dbProperties, cache)
+		return sqlite3.Open(ctx, conMan, dbProperties, cache)
 	case dbProperties.ConnectionString.IsPostgres():
 		return nil, fmt.Errorf("can't use Postgres implementation")
 	default:
diff --git a/setup/base/base.go b/setup/base/base.go
index dfe48ff3..e5fd6fed 100644
--- a/setup/base/base.go
+++ b/setup/base/base.go
@@ -17,254 +17,55 @@ package base
 import (
 	"bytes"
 	"context"
-	"database/sql"
 	"embed"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"html/template"
-	"io"
 	"io/fs"
 	"net"
 	"net/http"
 	_ "net/http/pprof"
 	"os"
 	"os/signal"
-	"sync"
 	"syscall"
 	"time"
 
-	"github.com/getsentry/sentry-go"
 	sentryhttp "github.com/getsentry/sentry-go/http"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"go.uber.org/atomic"
 
-	"github.com/matrix-org/dendrite/internal"
-	"github.com/matrix-org/dendrite/internal/caching"
-	"github.com/matrix-org/dendrite/internal/fulltext"
-	"github.com/matrix-org/dendrite/internal/httputil"
-	"github.com/matrix-org/dendrite/internal/pushgateway"
-	"github.com/matrix-org/dendrite/internal/sqlutil"
-
 	"github.com/gorilla/mux"
 	"github.com/kardianos/minwinsvc"
+	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/httputil"
 
 	"github.com/sirupsen/logrus"
 
 	"github.com/matrix-org/dendrite/setup/config"
-	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/setup/process"
 )
 
 //go:embed static/*.gotmpl
 var staticContent embed.FS
 
-// BaseDendrite is a base for creating new instances of dendrite. It parses
-// command line flags and config, and exposes methods for creating various
-// resources. All errors are handled by logging then exiting, so all methods
-// should only be used during start up.
-// Must be closed when shutting down.
-type BaseDendrite struct {
-	*process.ProcessContext
-	tracerCloser           io.Closer
-	PublicClientAPIMux     *mux.Router
-	PublicFederationAPIMux *mux.Router
-	PublicKeyAPIMux        *mux.Router
-	PublicMediaAPIMux      *mux.Router
-	PublicWellKnownAPIMux  *mux.Router
-	PublicStaticMux        *mux.Router
-	DendriteAdminMux       *mux.Router
-	SynapseAdminMux        *mux.Router
-	NATS                   *jetstream.NATSInstance
-	Cfg                    *config.Dendrite
-	Caches                 *caching.Caches
-	DNSCache               *gomatrixserverlib.DNSCache
-	Database               *sql.DB
-	DatabaseWriter         sqlutil.Writer
-	EnableMetrics          bool
-	Fulltext               *fulltext.Search
-	startupLock            sync.Mutex
-}
-
 const HTTPServerTimeout = time.Minute * 5
 
-type BaseDendriteOptions int
-
-const (
-	DisableMetrics BaseDendriteOptions = iota
-)
-
-// NewBaseDendrite creates a new instance to be used by a component.
-func NewBaseDendrite(cfg *config.Dendrite, options ...BaseDendriteOptions) *BaseDendrite {
-	platformSanityChecks()
-	enableMetrics := true
-	for _, opt := range options {
-		switch opt {
-		case DisableMetrics:
-			enableMetrics = false
-		}
-	}
-
-	configErrors := &config.ConfigErrors{}
-	cfg.Verify(configErrors)
-	if len(*configErrors) > 0 {
-		for _, err := range *configErrors {
-			logrus.Errorf("Configuration error: %s", err)
-		}
-		logrus.Fatalf("Failed to start due to configuration errors")
-	}
-
-	internal.SetupStdLogging()
-	internal.SetupHookLogging(cfg.Logging)
-	internal.SetupPprof()
-
-	logrus.Infof("Dendrite version %s", internal.VersionString())
-
-	if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
-		logrus.Warn("Open registration is enabled")
-	}
-
-	closer, err := cfg.SetupTracing()
-	if err != nil {
-		logrus.WithError(err).Panicf("failed to start opentracing")
-	}
-
-	var fts *fulltext.Search
-	if cfg.SyncAPI.Fulltext.Enabled {
-		fts, err = fulltext.New(cfg.SyncAPI.Fulltext)
-		if err != nil {
-			logrus.WithError(err).Panicf("failed to create full text")
-		}
-	}
-
-	if cfg.Global.Sentry.Enabled {
-		logrus.Info("Setting up Sentry for debugging...")
-		err = sentry.Init(sentry.ClientOptions{
-			Dsn:              cfg.Global.Sentry.DSN,
-			Environment:      cfg.Global.Sentry.Environment,
-			Debug:            true,
-			ServerName:       string(cfg.Global.ServerName),
-			Release:          "dendrite@" + internal.VersionString(),
-			AttachStacktrace: true,
-		})
-		if err != nil {
-			logrus.WithError(err).Panic("failed to start Sentry")
-		}
-	}
-
-	var dnsCache *gomatrixserverlib.DNSCache
-	if cfg.Global.DNSCache.Enabled {
-		dnsCache = gomatrixserverlib.NewDNSCache(
-			cfg.Global.DNSCache.CacheSize,
-			cfg.Global.DNSCache.CacheLifetime,
-		)
-		logrus.Infof(
-			"DNS cache enabled (size %d, lifetime %s)",
-			cfg.Global.DNSCache.CacheSize,
-			cfg.Global.DNSCache.CacheLifetime,
-		)
-	}
-
-	// If we're in monolith mode, we'll set up a global pool of database
-	// connections. A component is welcome to use this pool if they don't
-	// have a separate database config of their own.
-	var db *sql.DB
-	var writer sqlutil.Writer
-	if cfg.Global.DatabaseOptions.ConnectionString != "" {
-		if cfg.Global.DatabaseOptions.ConnectionString.IsSQLite() {
-			logrus.Panic("Using a global database connection pool is not supported with SQLite databases")
-		}
-		writer = sqlutil.NewDummyWriter()
-		if db, err = sqlutil.Open(&cfg.Global.DatabaseOptions, writer); err != nil {
-			logrus.WithError(err).Panic("Failed to set up global database connections")
-		}
-		logrus.Debug("Using global database connection pool")
-	}
-
-	// Ideally we would only use SkipClean on routes which we know can allow '/' but due to
-	// https://github.com/gorilla/mux/issues/460 we have to attach this at the top router.
-	// When used in conjunction with UseEncodedPath() we get the behaviour we want when parsing
-	// path parameters:
-	// /foo/bar%2Fbaz    == [foo, bar%2Fbaz]  (from UseEncodedPath)
-	// /foo/bar%2F%2Fbaz == [foo, bar%2F%2Fbaz] (from SkipClean)
-	// In particular, rooms v3 event IDs are not urlsafe and can include '/' and because they
-	// are randomly generated it results in flakey tests.
-	// We need to be careful with media APIs if they read from a filesystem to make sure they
-	// are not inadvertently reading paths without cleaning, else this could introduce a
-	// directory traversal attack e.g /../../../etc/passwd
-
-	return &BaseDendrite{
-		ProcessContext:         process.NewProcessContext(),
-		tracerCloser:           closer,
-		Cfg:                    cfg,
-		Caches:                 caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, enableMetrics),
-		DNSCache:               dnsCache,
-		PublicClientAPIMux:     mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicClientPathPrefix).Subrouter().UseEncodedPath(),
-		PublicFederationAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicFederationPathPrefix).Subrouter().UseEncodedPath(),
-		PublicKeyAPIMux:        mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicKeyPathPrefix).Subrouter().UseEncodedPath(),
-		PublicMediaAPIMux:      mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicMediaPathPrefix).Subrouter().UseEncodedPath(),
-		PublicWellKnownAPIMux:  mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicWellKnownPrefix).Subrouter().UseEncodedPath(),
-		PublicStaticMux:        mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicStaticPath).Subrouter().UseEncodedPath(),
-		DendriteAdminMux:       mux.NewRouter().SkipClean(true).PathPrefix(httputil.DendriteAdminPathPrefix).Subrouter().UseEncodedPath(),
-		SynapseAdminMux:        mux.NewRouter().SkipClean(true).PathPrefix(httputil.SynapseAdminPathPrefix).Subrouter().UseEncodedPath(),
-		NATS:                   &jetstream.NATSInstance{},
-		Database:               db,     // set if monolith with global connection pool only
-		DatabaseWriter:         writer, // set if monolith with global connection pool only
-		EnableMetrics:          enableMetrics,
-		Fulltext:               fts,
-	}
-}
-
-// Close implements io.Closer
-func (b *BaseDendrite) Close() error {
-	b.ProcessContext.ShutdownDendrite()
-	b.ProcessContext.WaitForShutdown()
-	return b.tracerCloser.Close()
-}
-
-// DatabaseConnection assists in setting up a database connection. It accepts
-// the database properties and a new writer for the given component. If we're
-// running in monolith mode with a global connection pool configured then we
-// will return that connection, along with the global writer, effectively
-// ignoring the options provided. Otherwise we'll open a new database connection
-// using the supplied options and writer. Note that it's possible for the pointer
-// receiver to be nil here – that's deliberate as some of the unit tests don't
-// have a BaseDendrite and just want a connection with the supplied config
-// without any pooling stuff.
-func (b *BaseDendrite) DatabaseConnection(dbProperties *config.DatabaseOptions, writer sqlutil.Writer) (*sql.DB, sqlutil.Writer, error) {
-	if dbProperties.ConnectionString != "" || b == nil {
-		// Open a new database connection using the supplied config.
-		db, err := sqlutil.Open(dbProperties, writer)
-		return db, writer, err
-	}
-	if b.Database != nil && b.DatabaseWriter != nil {
-		// Ignore the supplied config and return the global pool and
-		// writer.
-		return b.Database, b.DatabaseWriter, nil
-	}
-	return nil, nil, fmt.Errorf("no database connections configured")
-}
-
-// PushGatewayHTTPClient returns a new client for interacting with (external) Push Gateways.
-func (b *BaseDendrite) PushGatewayHTTPClient() pushgateway.Client {
-	return pushgateway.NewHTTPClient(b.Cfg.UserAPI.PushGatewayDisableTLSValidation)
-}
-
 // CreateClient creates a new client (normally used for media fetch requests).
 // Should only be called once per component.
-func (b *BaseDendrite) CreateClient() *gomatrixserverlib.Client {
-	if b.Cfg.Global.DisableFederation {
+func CreateClient(cfg *config.Dendrite, dnsCache *gomatrixserverlib.DNSCache) *gomatrixserverlib.Client {
+	if cfg.Global.DisableFederation {
 		return gomatrixserverlib.NewClient(
 			gomatrixserverlib.WithTransport(noOpHTTPTransport),
 		)
 	}
 	opts := []gomatrixserverlib.ClientOption{
-		gomatrixserverlib.WithSkipVerify(b.Cfg.FederationAPI.DisableTLSValidation),
+		gomatrixserverlib.WithSkipVerify(cfg.FederationAPI.DisableTLSValidation),
 		gomatrixserverlib.WithWellKnownSRVLookups(true),
 	}
-	if b.Cfg.Global.DNSCache.Enabled {
-		opts = append(opts, gomatrixserverlib.WithDNSCache(b.DNSCache))
+	if cfg.Global.DNSCache.Enabled && dnsCache != nil {
+		opts = append(opts, gomatrixserverlib.WithDNSCache(dnsCache))
 	}
 	client := gomatrixserverlib.NewClient(opts...)
 	client.SetUserAgent(fmt.Sprintf("Dendrite/%s", internal.VersionString()))
@@ -273,20 +74,20 @@ func (b *BaseDendrite) CreateClient() *gomatrixserverlib.Client {
 
 // CreateFederationClient creates a new federation client. Should only be called
 // once per component.
-func (b *BaseDendrite) CreateFederationClient() *gomatrixserverlib.FederationClient {
-	identities := b.Cfg.Global.SigningIdentities()
-	if b.Cfg.Global.DisableFederation {
+func CreateFederationClient(cfg *config.Dendrite, dnsCache *gomatrixserverlib.DNSCache) *gomatrixserverlib.FederationClient {
+	identities := cfg.Global.SigningIdentities()
+	if cfg.Global.DisableFederation {
 		return gomatrixserverlib.NewFederationClient(
 			identities, gomatrixserverlib.WithTransport(noOpHTTPTransport),
 		)
 	}
 	opts := []gomatrixserverlib.ClientOption{
 		gomatrixserverlib.WithTimeout(time.Minute * 5),
-		gomatrixserverlib.WithSkipVerify(b.Cfg.FederationAPI.DisableTLSValidation),
-		gomatrixserverlib.WithKeepAlives(!b.Cfg.FederationAPI.DisableHTTPKeepalives),
+		gomatrixserverlib.WithSkipVerify(cfg.FederationAPI.DisableTLSValidation),
+		gomatrixserverlib.WithKeepAlives(!cfg.FederationAPI.DisableHTTPKeepalives),
 	}
-	if b.Cfg.Global.DNSCache.Enabled {
-		opts = append(opts, gomatrixserverlib.WithDNSCache(b.DNSCache))
+	if cfg.Global.DNSCache.Enabled {
+		opts = append(opts, gomatrixserverlib.WithDNSCache(dnsCache))
 	}
 	client := gomatrixserverlib.NewFederationClient(
 		identities, opts...,
@@ -295,41 +96,12 @@ func (b *BaseDendrite) CreateFederationClient() *gomatrixserverlib.FederationCli
 	return client
 }
 
-func (b *BaseDendrite) configureHTTPErrors() {
-	notAllowedHandler := func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusMethodNotAllowed)
-		_, _ = w.Write([]byte(fmt.Sprintf("405 %s not allowed on this endpoint", r.Method)))
-	}
-
-	clientNotFoundHandler := func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusNotFound)
-		w.Header().Set("Content-Type", "application/json")
-		_, _ = w.Write([]byte(`{"errcode":"M_UNRECOGNIZED","error":"Unrecognized request"}`)) // nolint:misspell
-	}
-
-	notFoundCORSHandler := httputil.WrapHandlerInCORS(http.NotFoundHandler())
-	notAllowedCORSHandler := httputil.WrapHandlerInCORS(http.HandlerFunc(notAllowedHandler))
-
-	for _, router := range []*mux.Router{
-		b.PublicMediaAPIMux, b.DendriteAdminMux,
-		b.SynapseAdminMux, b.PublicWellKnownAPIMux,
-		b.PublicStaticMux,
-	} {
-		router.NotFoundHandler = notFoundCORSHandler
-		router.MethodNotAllowedHandler = notAllowedCORSHandler
-	}
-
-	// Special case so that we don't upset clients on the CS API.
-	b.PublicClientAPIMux.NotFoundHandler = http.HandlerFunc(clientNotFoundHandler)
-	b.PublicClientAPIMux.MethodNotAllowedHandler = http.HandlerFunc(clientNotFoundHandler)
-}
-
-func (b *BaseDendrite) ConfigureAdminEndpoints() {
-	b.DendriteAdminMux.HandleFunc("/monitor/up", func(w http.ResponseWriter, r *http.Request) {
+func ConfigureAdminEndpoints(processContext *process.ProcessContext, routers httputil.Routers) {
+	routers.DendriteAdmin.HandleFunc("/monitor/up", func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(200)
 	})
-	b.DendriteAdminMux.HandleFunc("/monitor/health", func(w http.ResponseWriter, r *http.Request) {
-		if isDegraded, reasons := b.ProcessContext.IsDegraded(); isDegraded {
+	routers.DendriteAdmin.HandleFunc("/monitor/health", func(w http.ResponseWriter, r *http.Request) {
+		if isDegraded, reasons := processContext.IsDegraded(); isDegraded {
 			w.WriteHeader(503)
 			_ = json.NewEncoder(w).Encode(struct {
 				Warnings []string `json:"warnings"`
@@ -344,14 +116,13 @@ func (b *BaseDendrite) ConfigureAdminEndpoints() {
 
 // SetupAndServeHTTP sets up the HTTP server to serve client & federation APIs
 // and adds a prometheus handler under /_dendrite/metrics.
-func (b *BaseDendrite) SetupAndServeHTTP(
+func SetupAndServeHTTP(
+	processContext *process.ProcessContext,
+	cfg *config.Dendrite,
+	routers httputil.Routers,
 	externalHTTPAddr config.ServerAddress,
 	certFile, keyFile *string,
 ) {
-	// Manually unlocked right before actually serving requests,
-	// as we don't return from this method (defer doesn't work).
-	b.startupLock.Lock()
-
 	externalRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
 
 	externalServ := &http.Server{
@@ -359,22 +130,20 @@ func (b *BaseDendrite) SetupAndServeHTTP(
 		WriteTimeout: HTTPServerTimeout,
 		Handler:      externalRouter,
 		BaseContext: func(_ net.Listener) context.Context {
-			return b.ProcessContext.Context()
+			return processContext.Context()
 		},
 	}
 
-	b.configureHTTPErrors()
-
 	//Redirect for Landing Page
 	externalRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, httputil.PublicStaticPath, http.StatusFound)
 	})
 
-	if b.Cfg.Global.Metrics.Enabled {
-		externalRouter.Handle("/metrics", httputil.WrapHandlerInBasicAuth(promhttp.Handler(), b.Cfg.Global.Metrics.BasicAuth))
+	if cfg.Global.Metrics.Enabled {
+		externalRouter.Handle("/metrics", httputil.WrapHandlerInBasicAuth(promhttp.Handler(), cfg.Global.Metrics.BasicAuth))
 	}
 
-	b.ConfigureAdminEndpoints()
+	ConfigureAdminEndpoints(processContext, routers)
 
 	// Parse and execute the landing page template
 	tmpl := template.Must(template.ParseFS(staticContent, "static/*.gotmpl"))
@@ -385,47 +154,48 @@ func (b *BaseDendrite) SetupAndServeHTTP(
 		logrus.WithError(err).Fatal("failed to execute landing page template")
 	}
 
-	b.PublicStaticMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	routers.Static.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		_, _ = w.Write(landingPage.Bytes())
 	})
 
 	var clientHandler http.Handler
-	clientHandler = b.PublicClientAPIMux
-	if b.Cfg.Global.Sentry.Enabled {
+	clientHandler = routers.Client
+	if cfg.Global.Sentry.Enabled {
 		sentryHandler := sentryhttp.New(sentryhttp.Options{
 			Repanic: true,
 		})
-		clientHandler = sentryHandler.Handle(b.PublicClientAPIMux)
+		clientHandler = sentryHandler.Handle(routers.Client)
 	}
 	var federationHandler http.Handler
-	federationHandler = b.PublicFederationAPIMux
-	if b.Cfg.Global.Sentry.Enabled {
+	federationHandler = routers.Federation
+	if cfg.Global.Sentry.Enabled {
 		sentryHandler := sentryhttp.New(sentryhttp.Options{
 			Repanic: true,
 		})
-		federationHandler = sentryHandler.Handle(b.PublicFederationAPIMux)
+		federationHandler = sentryHandler.Handle(routers.Federation)
 	}
-	externalRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(b.DendriteAdminMux)
+	externalRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
 	externalRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(clientHandler)
-	if !b.Cfg.Global.DisableFederation {
-		externalRouter.PathPrefix(httputil.PublicKeyPathPrefix).Handler(b.PublicKeyAPIMux)
+	if !cfg.Global.DisableFederation {
+		externalRouter.PathPrefix(httputil.PublicKeyPathPrefix).Handler(routers.Keys)
 		externalRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(federationHandler)
 	}
-	externalRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(b.SynapseAdminMux)
-	externalRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(b.PublicMediaAPIMux)
-	externalRouter.PathPrefix(httputil.PublicWellKnownPrefix).Handler(b.PublicWellKnownAPIMux)
-	externalRouter.PathPrefix(httputil.PublicStaticPath).Handler(b.PublicStaticMux)
+	externalRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
+	externalRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
+	externalRouter.PathPrefix(httputil.PublicWellKnownPrefix).Handler(routers.WellKnown)
+	externalRouter.PathPrefix(httputil.PublicStaticPath).Handler(routers.Static)
 
-	b.startupLock.Unlock()
+	externalRouter.NotFoundHandler = httputil.NotFoundCORSHandler
+	externalRouter.MethodNotAllowedHandler = httputil.NotAllowedHandler
 
 	if externalHTTPAddr.Enabled() {
 		go func() {
 			var externalShutdown atomic.Bool // RegisterOnShutdown can be called more than once
 			logrus.Infof("Starting external listener on %s", externalServ.Addr)
-			b.ProcessContext.ComponentStarted()
+			processContext.ComponentStarted()
 			externalServ.RegisterOnShutdown(func() {
 				if externalShutdown.CompareAndSwap(false, true) {
-					b.ProcessContext.ComponentFinished()
+					processContext.ComponentFinished()
 					logrus.Infof("Stopped external HTTP listener")
 				}
 			})
@@ -467,38 +237,27 @@ func (b *BaseDendrite) SetupAndServeHTTP(
 		}()
 	}
 
-	minwinsvc.SetOnExit(b.ProcessContext.ShutdownDendrite)
-	<-b.ProcessContext.WaitForShutdown()
+	minwinsvc.SetOnExit(processContext.ShutdownDendrite)
+	<-processContext.WaitForShutdown()
 
 	logrus.Infof("Stopping HTTP listeners")
 	_ = externalServ.Shutdown(context.Background())
 	logrus.Infof("Stopped HTTP listeners")
 }
 
-func (b *BaseDendrite) WaitForShutdown() {
+func WaitForShutdown(processCtx *process.ProcessContext) {
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 	select {
 	case <-sigs:
-	case <-b.ProcessContext.WaitForShutdown():
+	case <-processCtx.WaitForShutdown():
 	}
 	signal.Reset(syscall.SIGINT, syscall.SIGTERM)
 
 	logrus.Warnf("Shutdown signal received")
 
-	b.ProcessContext.ShutdownDendrite()
-	b.ProcessContext.WaitForComponentsToFinish()
-	if b.Cfg.Global.Sentry.Enabled {
-		if !sentry.Flush(time.Second * 5) {
-			logrus.Warnf("failed to flush all Sentry events!")
-		}
-	}
-	if b.Fulltext != nil {
-		err := b.Fulltext.Close()
-		if err != nil {
-			logrus.Warnf("failed to close full text search!")
-		}
-	}
+	processCtx.ShutdownDendrite()
+	processCtx.WaitForComponentsToFinish()
 
 	logrus.Warnf("Dendrite is exiting now")
 }
diff --git a/setup/base/sanity_other.go b/setup/base/sanity_other.go
index 48fe6e1f..d35c2e87 100644
--- a/setup/base/sanity_other.go
+++ b/setup/base/sanity_other.go
@@ -3,6 +3,6 @@
 
 package base
 
-func platformSanityChecks() {
+func PlatformSanityChecks() {
 	// Nothing to do yet.
 }
diff --git a/setup/base/sanity_unix.go b/setup/base/sanity_unix.go
index c630d3f1..0403df1a 100644
--- a/setup/base/sanity_unix.go
+++ b/setup/base/sanity_unix.go
@@ -9,7 +9,7 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-func platformSanityChecks() {
+func PlatformSanityChecks() {
 	// Dendrite needs a relatively high number of file descriptors in order
 	// to function properly, particularly when federating with lots of servers.
 	// If we run out of file descriptors, we might run into problems accessing
diff --git a/setup/config/config_address.go b/setup/config/config_address.go
index 0e4f0296..a35cc3f9 100644
--- a/setup/config/config_address.go
+++ b/setup/config/config_address.go
@@ -3,6 +3,7 @@ package config
 import (
 	"io/fs"
 	"net/url"
+	"strconv"
 )
 
 const (
@@ -32,8 +33,12 @@ func (s ServerAddress) Network() string {
 	}
 }
 
-func UnixSocketAddress(path string, perm fs.FileMode) ServerAddress {
-	return ServerAddress{Address: path, Scheme: NetworkUnix, UnixSocketPermission: perm}
+func UnixSocketAddress(path string, perm string) (ServerAddress, error) {
+	permission, err := strconv.ParseInt(perm, 8, 32)
+	if err != nil {
+		return ServerAddress{}, err
+	}
+	return ServerAddress{Address: path, Scheme: NetworkUnix, UnixSocketPermission: fs.FileMode(permission)}, nil
 }
 
 func HTTPAddress(urlAddress string) (ServerAddress, error) {
diff --git a/setup/jetstream/nats.go b/setup/jetstream/nats.go
index 01fec9ad..06a58d54 100644
--- a/setup/jetstream/nats.go
+++ b/setup/jetstream/nats.go
@@ -20,6 +20,8 @@ import (
 
 type NATSInstance struct {
 	*natsserver.Server
+	nc *natsclient.Conn
+	js natsclient.JetStreamContext
 }
 
 var natsLock sync.Mutex
@@ -54,7 +56,9 @@ func (s *NATSInstance) Prepare(process *process.ProcessContext, cfg *config.JetS
 		if err != nil {
 			panic(err)
 		}
-		s.SetLogger(NewLogAdapter(), opts.Debug, opts.Trace)
+		if !cfg.NoLog {
+			s.SetLogger(NewLogAdapter(), opts.Debug, opts.Trace)
+		}
 		go func() {
 			process.ComponentStarted()
 			s.Start()
@@ -69,11 +73,18 @@ func (s *NATSInstance) Prepare(process *process.ProcessContext, cfg *config.JetS
 	if !s.ReadyForConnections(time.Second * 10) {
 		logrus.Fatalln("NATS did not start in time")
 	}
+	// reuse existing connections
+	if s.nc != nil {
+		return s.js, s.nc
+	}
 	nc, err := natsclient.Connect("", natsclient.InProcessServer(s))
 	if err != nil {
 		logrus.Fatalln("Failed to create NATS client")
 	}
-	return setupNATS(process, cfg, nc)
+	js, _ := setupNATS(process, cfg, nc)
+	s.js = js
+	s.nc = nc
+	return js, nc
 }
 
 func setupNATS(process *process.ProcessContext, cfg *config.JetStream, nc *natsclient.Conn) (natsclient.JetStreamContext, *natsclient.Conn) {
diff --git a/setup/monolith.go b/setup/monolith.go
index d8c65223..51e40070 100644
--- a/setup/monolith.go
+++ b/setup/monolith.go
@@ -20,13 +20,17 @@ import (
 	"github.com/matrix-org/dendrite/clientapi/api"
 	"github.com/matrix-org/dendrite/federationapi"
 	federationAPI "github.com/matrix-org/dendrite/federationapi/api"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/internal/transactions"
 	"github.com/matrix-org/dendrite/mediaapi"
 	"github.com/matrix-org/dendrite/relayapi"
 	relayAPI "github.com/matrix-org/dendrite/relayapi/api"
 	roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/syncapi"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
@@ -52,23 +56,31 @@ type Monolith struct {
 }
 
 // AddAllPublicRoutes attaches all public paths to the given router
-func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite) {
+func (m *Monolith) AddAllPublicRoutes(
+	processCtx *process.ProcessContext,
+	cfg *config.Dendrite,
+	routers httputil.Routers,
+	cm sqlutil.Connections,
+	natsInstance *jetstream.NATSInstance,
+	caches *caching.Caches,
+	enableMetrics bool,
+) {
 	userDirectoryProvider := m.ExtUserDirectoryProvider
 	if userDirectoryProvider == nil {
 		userDirectoryProvider = m.UserAPI
 	}
 	clientapi.AddPublicRoutes(
-		base, m.FedClient, m.RoomserverAPI, m.AppserviceAPI, transactions.New(),
+		processCtx, routers, cfg, natsInstance, m.FedClient, m.RoomserverAPI, m.AppserviceAPI, transactions.New(),
 		m.FederationAPI, m.UserAPI, userDirectoryProvider,
-		m.ExtPublicRoomsProvider,
+		m.ExtPublicRoomsProvider, enableMetrics,
 	)
 	federationapi.AddPublicRoutes(
-		base, m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, nil,
+		processCtx, routers, cfg, natsInstance, m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, nil, enableMetrics,
 	)
-	mediaapi.AddPublicRoutes(base, m.UserAPI, m.Client)
-	syncapi.AddPublicRoutes(base, m.UserAPI, m.RoomserverAPI)
+	mediaapi.AddPublicRoutes(routers.Media, cm, cfg, m.UserAPI, m.Client)
+	syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, m.UserAPI, m.RoomserverAPI, caches, enableMetrics)
 
 	if m.RelayAPI != nil {
-		relayapi.AddPublicRoutes(base, m.KeyRing, m.RelayAPI)
+		relayapi.AddPublicRoutes(routers, cfg, m.KeyRing, m.RelayAPI)
 	}
 }
diff --git a/setup/mscs/msc2836/msc2836.go b/setup/mscs/msc2836/msc2836.go
index 4bb6a5ee..92248bc1 100644
--- a/setup/mscs/msc2836/msc2836.go
+++ b/setup/mscs/msc2836/msc2836.go
@@ -31,8 +31,9 @@ import (
 	fs "github.com/matrix-org/dendrite/federationapi/api"
 	"github.com/matrix-org/dendrite/internal/hooks"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	roomserver "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -99,10 +100,10 @@ func toClientResponse(res *MSC2836EventRelationshipsResponse) *EventRelationship
 
 // Enable this MSC
 func Enable(
-	base *base.BaseDendrite, rsAPI roomserver.RoomserverInternalAPI, fsAPI fs.FederationInternalAPI,
+	cfg *config.Dendrite, cm sqlutil.Connections, routers httputil.Routers, rsAPI roomserver.RoomserverInternalAPI, fsAPI fs.FederationInternalAPI,
 	userAPI userapi.UserInternalAPI, keyRing gomatrixserverlib.JSONVerifier,
 ) error {
-	db, err := NewDatabase(base, &base.Cfg.MSCs.Database)
+	db, err := NewDatabase(cm, &cfg.MSCs.Database)
 	if err != nil {
 		return fmt.Errorf("cannot enable MSC2836: %w", err)
 	}
@@ -125,14 +126,14 @@ func Enable(
 		}
 	})
 
-	base.PublicClientAPIMux.Handle("/unstable/event_relationships",
+	routers.Client.Handle("/unstable/event_relationships",
 		httputil.MakeAuthAPI("eventRelationships", userAPI, eventRelationshipHandler(db, rsAPI, fsAPI)),
 	).Methods(http.MethodPost, http.MethodOptions)
 
-	base.PublicFederationAPIMux.Handle("/unstable/event_relationships", httputil.MakeExternalAPI(
+	routers.Federation.Handle("/unstable/event_relationships", httputil.MakeExternalAPI(
 		"msc2836_event_relationships", func(req *http.Request) util.JSONResponse {
 			fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest(
-				req, time.Now(), base.Cfg.Global.ServerName, base.Cfg.Global.IsLocalServerName, keyRing,
+				req, time.Now(), cfg.Global.ServerName, cfg.Global.IsLocalServerName, keyRing,
 			)
 			if fedReq == nil {
 				return errResp
diff --git a/setup/mscs/msc2836/storage.go b/setup/mscs/msc2836/storage.go
index 827e82f7..1cf7e878 100644
--- a/setup/mscs/msc2836/storage.go
+++ b/setup/mscs/msc2836/storage.go
@@ -8,7 +8,6 @@ import (
 	"encoding/json"
 
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -59,17 +58,17 @@ type DB struct {
 }
 
 // NewDatabase loads the database for msc2836
-func NewDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions) (Database, error) {
+func NewDatabase(conMan sqlutil.Connections, dbOpts *config.DatabaseOptions) (Database, error) {
 	if dbOpts.ConnectionString.IsPostgres() {
-		return newPostgresDatabase(base, dbOpts)
+		return newPostgresDatabase(conMan, dbOpts)
 	}
-	return newSQLiteDatabase(base, dbOpts)
+	return newSQLiteDatabase(conMan, dbOpts)
 }
 
-func newPostgresDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions) (Database, error) {
+func newPostgresDatabase(conMan sqlutil.Connections, dbOpts *config.DatabaseOptions) (Database, error) {
 	d := DB{}
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbOpts, sqlutil.NewDummyWriter()); err != nil {
+	if d.db, d.writer, err = conMan.Connection(dbOpts); err != nil {
 		return nil, err
 	}
 	_, err = d.db.Exec(`
@@ -144,10 +143,10 @@ func newPostgresDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions
 	return &d, err
 }
 
-func newSQLiteDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions) (Database, error) {
+func newSQLiteDatabase(conMan sqlutil.Connections, dbOpts *config.DatabaseOptions) (Database, error) {
 	d := DB{}
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbOpts, sqlutil.NewExclusiveWriter()); err != nil {
+	if d.db, d.writer, err = conMan.Connection(dbOpts); err != nil {
 		return nil, err
 	}
 	_, err = d.db.Exec(`
diff --git a/setup/mscs/msc2946/msc2946.go b/setup/mscs/msc2946/msc2946.go
index 56c06359..d33897b9 100644
--- a/setup/mscs/msc2946/msc2946.go
+++ b/setup/mscs/msc2946/msc2946.go
@@ -33,7 +33,7 @@ import (
 	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
 	roomserver "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -54,17 +54,17 @@ type MSC2946ClientResponse struct {
 
 // Enable this MSC
 func Enable(
-	base *base.BaseDendrite, rsAPI roomserver.RoomserverInternalAPI, userAPI userapi.UserInternalAPI,
+	cfg *config.Dendrite, routers httputil.Routers, rsAPI roomserver.RoomserverInternalAPI, userAPI userapi.UserInternalAPI,
 	fsAPI fs.FederationInternalAPI, keyRing gomatrixserverlib.JSONVerifier, cache caching.SpaceSummaryRoomsCache,
 ) error {
-	clientAPI := httputil.MakeAuthAPI("spaces", userAPI, spacesHandler(rsAPI, fsAPI, cache, base.Cfg.Global.ServerName), httputil.WithAllowGuests())
-	base.PublicClientAPIMux.Handle("/v1/rooms/{roomID}/hierarchy", clientAPI).Methods(http.MethodGet, http.MethodOptions)
-	base.PublicClientAPIMux.Handle("/unstable/org.matrix.msc2946/rooms/{roomID}/hierarchy", clientAPI).Methods(http.MethodGet, http.MethodOptions)
+	clientAPI := httputil.MakeAuthAPI("spaces", userAPI, spacesHandler(rsAPI, fsAPI, cache, cfg.Global.ServerName), httputil.WithAllowGuests())
+	routers.Client.Handle("/v1/rooms/{roomID}/hierarchy", clientAPI).Methods(http.MethodGet, http.MethodOptions)
+	routers.Client.Handle("/unstable/org.matrix.msc2946/rooms/{roomID}/hierarchy", clientAPI).Methods(http.MethodGet, http.MethodOptions)
 
 	fedAPI := httputil.MakeExternalAPI(
 		"msc2946_fed_spaces", func(req *http.Request) util.JSONResponse {
 			fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest(
-				req, time.Now(), base.Cfg.Global.ServerName, base.Cfg.Global.IsLocalServerName, keyRing,
+				req, time.Now(), cfg.Global.ServerName, cfg.Global.IsLocalServerName, keyRing,
 			)
 			if fedReq == nil {
 				return errResp
@@ -75,11 +75,11 @@ func Enable(
 				return util.ErrorResponse(err)
 			}
 			roomID := params["roomID"]
-			return federatedSpacesHandler(req.Context(), fedReq, roomID, cache, rsAPI, fsAPI, base.Cfg.Global.ServerName)
+			return federatedSpacesHandler(req.Context(), fedReq, roomID, cache, rsAPI, fsAPI, cfg.Global.ServerName)
 		},
 	)
-	base.PublicFederationAPIMux.Handle("/unstable/org.matrix.msc2946/hierarchy/{roomID}", fedAPI).Methods(http.MethodGet)
-	base.PublicFederationAPIMux.Handle("/v1/hierarchy/{roomID}", fedAPI).Methods(http.MethodGet)
+	routers.Federation.Handle("/unstable/org.matrix.msc2946/hierarchy/{roomID}", fedAPI).Methods(http.MethodGet)
+	routers.Federation.Handle("/v1/hierarchy/{roomID}", fedAPI).Methods(http.MethodGet)
 	return nil
 }
 
diff --git a/setup/mscs/mscs.go b/setup/mscs/mscs.go
index 35b7bba3..9cd5eed1 100644
--- a/setup/mscs/mscs.go
+++ b/setup/mscs/mscs.go
@@ -19,30 +19,33 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup"
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/mscs/msc2836"
 	"github.com/matrix-org/dendrite/setup/mscs/msc2946"
 	"github.com/matrix-org/util"
 )
 
 // Enable MSCs - returns an error on unknown MSCs
-func Enable(base *base.BaseDendrite, monolith *setup.Monolith) error {
-	for _, msc := range base.Cfg.MSCs.MSCs {
+func Enable(cfg *config.Dendrite, cm sqlutil.Connections, routers httputil.Routers, monolith *setup.Monolith, caches *caching.Caches) error {
+	for _, msc := range cfg.MSCs.MSCs {
 		util.GetLogger(context.Background()).WithField("msc", msc).Info("Enabling MSC")
-		if err := EnableMSC(base, monolith, msc); err != nil {
+		if err := EnableMSC(cfg, cm, routers, monolith, msc, caches); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func EnableMSC(base *base.BaseDendrite, monolith *setup.Monolith, msc string) error {
+func EnableMSC(cfg *config.Dendrite, cm sqlutil.Connections, routers httputil.Routers, monolith *setup.Monolith, msc string, caches *caching.Caches) error {
 	switch msc {
 	case "msc2836":
-		return msc2836.Enable(base, monolith.RoomserverAPI, monolith.FederationAPI, monolith.UserAPI, monolith.KeyRing)
+		return msc2836.Enable(cfg, cm, routers, monolith.RoomserverAPI, monolith.FederationAPI, monolith.UserAPI, monolith.KeyRing)
 	case "msc2946":
-		return msc2946.Enable(base, monolith.RoomserverAPI, monolith.UserAPI, monolith.FederationAPI, monolith.KeyRing, base.Caches)
+		return msc2946.Enable(cfg, routers, monolith.RoomserverAPI, monolith.UserAPI, monolith.FederationAPI, monolith.KeyRing, caches)
 	case "msc2444": // enabled inside federationapi
 	case "msc2753": // enabled inside clientapi
 	default:
diff --git a/syncapi/consumers/clientapi.go b/syncapi/consumers/clientapi.go
index 735f6718..43dc0f51 100644
--- a/syncapi/consumers/clientapi.go
+++ b/syncapi/consumers/clientapi.go
@@ -50,7 +50,7 @@ type OutputClientDataConsumer struct {
 	stream       streams.StreamProvider
 	notifier     *notifier.Notifier
 	serverName   gomatrixserverlib.ServerName
-	fts          *fulltext.Search
+	fts          fulltext.Indexer
 	cfg          *config.SyncAPI
 }
 
diff --git a/syncapi/consumers/roomserver.go b/syncapi/consumers/roomserver.go
index a8d4d2b2..21f6104d 100644
--- a/syncapi/consumers/roomserver.go
+++ b/syncapi/consumers/roomserver.go
@@ -51,7 +51,7 @@ type OutputRoomEventConsumer struct {
 	pduStream    streams.StreamProvider
 	inviteStream streams.StreamProvider
 	notifier     *notifier.Notifier
-	fts          *fulltext.Search
+	fts          fulltext.Indexer
 }
 
 // NewOutputRoomEventConsumer creates a new OutputRoomEventConsumer. Call Start() to begin consuming from room servers.
diff --git a/syncapi/routing/routing.go b/syncapi/routing/routing.go
index 4cc1a6a8..b1283247 100644
--- a/syncapi/routing/routing.go
+++ b/syncapi/routing/routing.go
@@ -43,7 +43,7 @@ func Setup(
 	rsAPI api.SyncRoomserverAPI,
 	cfg *config.SyncAPI,
 	lazyLoadCache caching.LazyLoadCache,
-	fts *fulltext.Search,
+	fts fulltext.Indexer,
 ) {
 	v1unstablemux := csMux.PathPrefix("/{apiversion:(?:v1|unstable)}/").Subrouter()
 	v3mux := csMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter()
diff --git a/syncapi/routing/search.go b/syncapi/routing/search.go
index 081ec6cb..13625b9c 100644
--- a/syncapi/routing/search.go
+++ b/syncapi/routing/search.go
@@ -37,7 +37,7 @@ import (
 )
 
 // nolint:gocyclo
-func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts *fulltext.Search, from *string) util.JSONResponse {
+func Search(req *http.Request, device *api.Device, syncDB storage.Database, fts fulltext.Indexer, from *string) util.JSONResponse {
 	start := time.Now()
 	var (
 		searchReq SearchRequest
diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go
index aa54cb08..8147b486 100644
--- a/syncapi/storage/postgres/account_data_table.go
+++ b/syncapi/storage/postgres/account_data_table.go
@@ -78,16 +78,11 @@ func NewPostgresAccountDataTable(db *sql.DB) (tables.AccountData, error) {
 	if err != nil {
 		return nil, err
 	}
-	if s.insertAccountDataStmt, err = db.Prepare(insertAccountDataSQL); err != nil {
-		return nil, err
-	}
-	if s.selectAccountDataInRangeStmt, err = db.Prepare(selectAccountDataInRangeSQL); err != nil {
-		return nil, err
-	}
-	if s.selectMaxAccountDataIDStmt, err = db.Prepare(selectMaxAccountDataIDSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.insertAccountDataStmt, insertAccountDataSQL},
+		{&s.selectAccountDataInRangeStmt, selectAccountDataInRangeSQL},
+		{&s.selectMaxAccountDataIDStmt, selectMaxAccountDataIDSQL},
+	}.Prepare(db)
 }
 
 func (s *accountDataStatements) InsertAccountData(
diff --git a/syncapi/storage/postgres/filter_table.go b/syncapi/storage/postgres/filter_table.go
index 86cec362..44c3de72 100644
--- a/syncapi/storage/postgres/filter_table.go
+++ b/syncapi/storage/postgres/filter_table.go
@@ -61,16 +61,11 @@ func NewPostgresFilterTable(db *sql.DB) (tables.Filter, error) {
 		return nil, err
 	}
 	s := &filterStatements{}
-	if s.selectFilterStmt, err = db.Prepare(selectFilterSQL); err != nil {
-		return nil, err
-	}
-	if s.selectFilterIDByContentStmt, err = db.Prepare(selectFilterIDByContentSQL); err != nil {
-		return nil, err
-	}
-	if s.insertFilterStmt, err = db.Prepare(insertFilterSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.selectFilterStmt, selectFilterSQL},
+		{&s.selectFilterIDByContentStmt, selectFilterIDByContentSQL},
+		{&s.insertFilterStmt, insertFilterSQL},
+	}.Prepare(db)
 }
 
 func (s *filterStatements) SelectFilter(
diff --git a/syncapi/storage/postgres/ignores_table.go b/syncapi/storage/postgres/ignores_table.go
index 97660725..a511c747 100644
--- a/syncapi/storage/postgres/ignores_table.go
+++ b/syncapi/storage/postgres/ignores_table.go
@@ -52,13 +52,11 @@ func NewPostgresIgnoresTable(db *sql.DB) (tables.Ignores, error) {
 		return nil, err
 	}
 	s := &ignoresStatements{}
-	if s.selectIgnoresStmt, err = db.Prepare(selectIgnoresSQL); err != nil {
-		return nil, err
-	}
-	if s.upsertIgnoresStmt, err = db.Prepare(upsertIgnoresSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+
+	return s, sqlutil.StatementList{
+		{&s.selectIgnoresStmt, selectIgnoresSQL},
+		{&s.upsertIgnoresStmt, upsertIgnoresSQL},
+	}.Prepare(db)
 }
 
 func (s *ignoresStatements) SelectIgnores(
diff --git a/syncapi/storage/postgres/send_to_device_table.go b/syncapi/storage/postgres/send_to_device_table.go
index 6ab1f0f4..88b86ef7 100644
--- a/syncapi/storage/postgres/send_to_device_table.go
+++ b/syncapi/storage/postgres/send_to_device_table.go
@@ -88,19 +88,12 @@ func NewPostgresSendToDeviceTable(db *sql.DB) (tables.SendToDevice, error) {
 	if err != nil {
 		return nil, err
 	}
-	if s.insertSendToDeviceMessageStmt, err = db.Prepare(insertSendToDeviceMessageSQL); err != nil {
-		return nil, err
-	}
-	if s.selectSendToDeviceMessagesStmt, err = db.Prepare(selectSendToDeviceMessagesSQL); err != nil {
-		return nil, err
-	}
-	if s.deleteSendToDeviceMessagesStmt, err = db.Prepare(deleteSendToDeviceMessagesSQL); err != nil {
-		return nil, err
-	}
-	if s.selectMaxSendToDeviceIDStmt, err = db.Prepare(selectMaxSendToDeviceIDSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.insertSendToDeviceMessageStmt, insertSendToDeviceMessageSQL},
+		{&s.selectSendToDeviceMessagesStmt, selectSendToDeviceMessagesSQL},
+		{&s.deleteSendToDeviceMessagesStmt, deleteSendToDeviceMessagesSQL},
+		{&s.selectMaxSendToDeviceIDStmt, selectMaxSendToDeviceIDSQL},
+	}.Prepare(db)
 }
 
 func (s *sendToDeviceStatements) InsertSendToDeviceMessage(
diff --git a/syncapi/storage/postgres/syncserver.go b/syncapi/storage/postgres/syncserver.go
index 850d24a0..9f9de28d 100644
--- a/syncapi/storage/postgres/syncserver.go
+++ b/syncapi/storage/postgres/syncserver.go
@@ -16,12 +16,12 @@
 package postgres
 
 import (
+	"context"
 	"database/sql"
 
 	// Import the postgres database driver.
 	_ "github.com/lib/pq"
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/syncapi/storage/postgres/deltas"
 	"github.com/matrix-org/dendrite/syncapi/storage/shared"
@@ -36,10 +36,10 @@ type SyncServerDatasource struct {
 }
 
 // NewDatabase creates a new sync server database
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) {
+func NewDatabase(ctx context.Context, cm sqlutil.Connections, dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) {
 	var d SyncServerDatasource
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()); err != nil {
+	if d.db, d.writer, err = cm.Connection(dbProperties); err != nil {
 		return nil, err
 	}
 	accountData, err := NewPostgresAccountDataTable(d.db)
@@ -111,7 +111,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions)
 			Up:      deltas.UpSetHistoryVisibility, // Requires current_room_state and output_room_events to be created.
 		},
 	)
-	err = m.Up(base.Context())
+	err = m.Up(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go
index de0e72db..eaf52234 100644
--- a/syncapi/storage/sqlite3/account_data_table.go
+++ b/syncapi/storage/sqlite3/account_data_table.go
@@ -66,16 +66,11 @@ func NewSqliteAccountDataTable(db *sql.DB, streamID *StreamIDStatements) (tables
 	if err != nil {
 		return nil, err
 	}
-	if s.insertAccountDataStmt, err = db.Prepare(insertAccountDataSQL); err != nil {
-		return nil, err
-	}
-	if s.selectMaxAccountDataIDStmt, err = db.Prepare(selectMaxAccountDataIDSQL); err != nil {
-		return nil, err
-	}
-	if s.selectAccountDataInRangeStmt, err = db.Prepare(selectAccountDataInRangeSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.insertAccountDataStmt, insertAccountDataSQL},
+		{&s.selectMaxAccountDataIDStmt, selectMaxAccountDataIDSQL},
+		{&s.selectAccountDataInRangeStmt, selectAccountDataInRangeSQL},
+	}.Prepare(db)
 }
 
 func (s *accountDataStatements) InsertAccountData(
diff --git a/syncapi/storage/sqlite3/filter_table.go b/syncapi/storage/sqlite3/filter_table.go
index 5f1e980e..0132a766 100644
--- a/syncapi/storage/sqlite3/filter_table.go
+++ b/syncapi/storage/sqlite3/filter_table.go
@@ -65,16 +65,11 @@ func NewSqliteFilterTable(db *sql.DB) (tables.Filter, error) {
 	s := &filterStatements{
 		db: db,
 	}
-	if s.selectFilterStmt, err = db.Prepare(selectFilterSQL); err != nil {
-		return nil, err
-	}
-	if s.selectFilterIDByContentStmt, err = db.Prepare(selectFilterIDByContentSQL); err != nil {
-		return nil, err
-	}
-	if s.insertFilterStmt, err = db.Prepare(insertFilterSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.selectFilterStmt, selectFilterSQL},
+		{&s.selectFilterIDByContentStmt, selectFilterIDByContentSQL},
+		{&s.insertFilterStmt, insertFilterSQL},
+	}.Prepare(db)
 }
 
 func (s *filterStatements) SelectFilter(
diff --git a/syncapi/storage/sqlite3/ignores_table.go b/syncapi/storage/sqlite3/ignores_table.go
index 5ee1a9fa..bff5780b 100644
--- a/syncapi/storage/sqlite3/ignores_table.go
+++ b/syncapi/storage/sqlite3/ignores_table.go
@@ -52,13 +52,10 @@ func NewSqliteIgnoresTable(db *sql.DB) (tables.Ignores, error) {
 		return nil, err
 	}
 	s := &ignoresStatements{}
-	if s.selectIgnoresStmt, err = db.Prepare(selectIgnoresSQL); err != nil {
-		return nil, err
-	}
-	if s.upsertIgnoresStmt, err = db.Prepare(upsertIgnoresSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.selectIgnoresStmt, selectIgnoresSQL},
+		{&s.upsertIgnoresStmt, upsertIgnoresSQL},
+	}.Prepare(db)
 }
 
 func (s *ignoresStatements) SelectIgnores(
diff --git a/syncapi/storage/sqlite3/send_to_device_table.go b/syncapi/storage/sqlite3/send_to_device_table.go
index 0da06506..998a063c 100644
--- a/syncapi/storage/sqlite3/send_to_device_table.go
+++ b/syncapi/storage/sqlite3/send_to_device_table.go
@@ -88,19 +88,12 @@ func NewSqliteSendToDeviceTable(db *sql.DB) (tables.SendToDevice, error) {
 	if err != nil {
 		return nil, err
 	}
-	if s.insertSendToDeviceMessageStmt, err = db.Prepare(insertSendToDeviceMessageSQL); err != nil {
-		return nil, err
-	}
-	if s.selectSendToDeviceMessagesStmt, err = db.Prepare(selectSendToDeviceMessagesSQL); err != nil {
-		return nil, err
-	}
-	if s.deleteSendToDeviceMessagesStmt, err = db.Prepare(deleteSendToDeviceMessagesSQL); err != nil {
-		return nil, err
-	}
-	if s.selectMaxSendToDeviceIDStmt, err = db.Prepare(selectMaxSendToDeviceIDSQL); err != nil {
-		return nil, err
-	}
-	return s, nil
+	return s, sqlutil.StatementList{
+		{&s.insertSendToDeviceMessageStmt, insertSendToDeviceMessageSQL},
+		{&s.selectSendToDeviceMessagesStmt, selectSendToDeviceMessagesSQL},
+		{&s.deleteSendToDeviceMessagesStmt, deleteSendToDeviceMessagesSQL},
+		{&s.selectMaxSendToDeviceIDStmt, selectMaxSendToDeviceIDSQL},
+	}.Prepare(db)
 }
 
 func (s *sendToDeviceStatements) InsertSendToDeviceMessage(
diff --git a/syncapi/storage/sqlite3/stream_id_table.go b/syncapi/storage/sqlite3/stream_id_table.go
index a4bba508..b51eccf5 100644
--- a/syncapi/storage/sqlite3/stream_id_table.go
+++ b/syncapi/storage/sqlite3/stream_id_table.go
@@ -47,10 +47,9 @@ func (s *StreamIDStatements) Prepare(db *sql.DB) (err error) {
 	if err != nil {
 		return
 	}
-	if s.increaseStreamIDStmt, err = db.Prepare(increaseStreamIDStmt); err != nil {
-		return
-	}
-	return
+	return sqlutil.StatementList{
+		{&s.increaseStreamIDStmt, increaseStreamIDStmt},
+	}.Prepare(db)
 }
 
 func (s *StreamIDStatements) nextPDUID(ctx context.Context, txn *sql.Tx) (pos types.StreamPosition, err error) {
diff --git a/syncapi/storage/sqlite3/syncserver.go b/syncapi/storage/sqlite3/syncserver.go
index 51054690..3f1ca355 100644
--- a/syncapi/storage/sqlite3/syncserver.go
+++ b/syncapi/storage/sqlite3/syncserver.go
@@ -20,7 +20,6 @@ import (
 	"database/sql"
 
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/syncapi/storage/shared"
 	"github.com/matrix-org/dendrite/syncapi/storage/sqlite3/deltas"
@@ -37,13 +36,14 @@ type SyncServerDatasource struct {
 
 // NewDatabase creates a new sync server database
 // nolint: gocyclo
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) {
+func NewDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) {
 	var d SyncServerDatasource
 	var err error
-	if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()); err != nil {
+
+	if d.db, d.writer, err = conMan.Connection(dbProperties); err != nil {
 		return nil, err
 	}
-	if err = d.prepare(base.Context()); err != nil {
+	if err = d.prepare(ctx); err != nil {
 		return nil, err
 	}
 	return &d, nil
diff --git a/syncapi/storage/storage.go b/syncapi/storage/storage.go
index 5b20c6cc..8714ec5e 100644
--- a/syncapi/storage/storage.go
+++ b/syncapi/storage/storage.go
@@ -18,21 +18,22 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/syncapi/storage/postgres"
 	"github.com/matrix-org/dendrite/syncapi/storage/sqlite3"
 )
 
 // NewSyncServerDatasource opens a database connection.
-func NewSyncServerDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) {
+func NewSyncServerDatasource(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties)
+		return sqlite3.NewDatabase(ctx, conMan, dbProperties)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.NewDatabase(base, dbProperties)
+		return postgres.NewDatabase(ctx, conMan, dbProperties)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
diff --git a/syncapi/storage/storage_wasm.go b/syncapi/storage/storage_wasm.go
index c1544474..db0b173b 100644
--- a/syncapi/storage/storage_wasm.go
+++ b/syncapi/storage/storage_wasm.go
@@ -15,18 +15,19 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/syncapi/storage/sqlite3"
 )
 
 // NewPublicRoomsServerDatabase opens a database connection.
-func NewSyncServerDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) {
+func NewSyncServerDatasource(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (Database, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewDatabase(base, dbProperties)
+		return sqlite3.NewDatabase(ctx, conMan, dbProperties)
 	case dbProperties.ConnectionString.IsPostgres():
 		return nil, fmt.Errorf("can't use Postgres implementation")
 	default:
diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go
index 153f7af5..ecbe05dd 100644
--- a/syncapi/syncapi.go
+++ b/syncapi/syncapi.go
@@ -17,12 +17,16 @@ package syncapi
 import (
 	"context"
 
+	"github.com/matrix-org/dendrite/internal/fulltext"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/sirupsen/logrus"
 
 	"github.com/matrix-org/dendrite/internal/caching"
 
 	"github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
 
@@ -38,45 +42,57 @@ import (
 // AddPublicRoutes sets up and registers HTTP handlers for the SyncAPI
 // component.
 func AddPublicRoutes(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	routers httputil.Routers,
+	dendriteCfg *config.Dendrite,
+	cm sqlutil.Connections,
+	natsInstance *jetstream.NATSInstance,
 	userAPI userapi.SyncUserAPI,
 	rsAPI api.SyncRoomserverAPI,
+	caches caching.LazyLoadCache,
+	enableMetrics bool,
 ) {
-	cfg := &base.Cfg.SyncAPI
+	js, natsClient := natsInstance.Prepare(processContext, &dendriteCfg.Global.JetStream)
 
-	js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
-
-	syncDB, err := storage.NewSyncServerDatasource(base, &cfg.Database)
+	syncDB, err := storage.NewSyncServerDatasource(processContext.Context(), cm, &dendriteCfg.SyncAPI.Database)
 	if err != nil {
 		logrus.WithError(err).Panicf("failed to connect to sync db")
 	}
 
 	eduCache := caching.NewTypingCache()
 	notifier := notifier.NewNotifier()
-	streams := streams.NewSyncStreamProviders(syncDB, userAPI, rsAPI, eduCache, base.Caches, notifier)
+	streams := streams.NewSyncStreamProviders(syncDB, userAPI, rsAPI, eduCache, caches, notifier)
 	notifier.SetCurrentPosition(streams.Latest(context.Background()))
 	if err = notifier.Load(context.Background(), syncDB); err != nil {
 		logrus.WithError(err).Panicf("failed to load notifier ")
 	}
 
+	var fts *fulltext.Search
+	if dendriteCfg.SyncAPI.Fulltext.Enabled {
+		fts, err = fulltext.New(processContext, dendriteCfg.SyncAPI.Fulltext)
+		if err != nil {
+			logrus.WithError(err).Panicf("failed to create full text")
+		}
+	}
+
 	federationPresenceProducer := &producers.FederationAPIPresenceProducer{
-		Topic:     cfg.Matrix.JetStream.Prefixed(jetstream.OutputPresenceEvent),
+		Topic:     dendriteCfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
 		JetStream: js,
 	}
 	presenceConsumer := consumers.NewPresenceConsumer(
-		base.ProcessContext, cfg, js, natsClient, syncDB,
+		processContext, &dendriteCfg.SyncAPI, js, natsClient, syncDB,
 		notifier, streams.PresenceStreamProvider,
 		userAPI,
 	)
 
-	requestPool := sync.NewRequestPool(syncDB, cfg, userAPI, rsAPI, streams, notifier, federationPresenceProducer, presenceConsumer, base.EnableMetrics)
+	requestPool := sync.NewRequestPool(syncDB, &dendriteCfg.SyncAPI, userAPI, rsAPI, streams, notifier, federationPresenceProducer, presenceConsumer, enableMetrics)
 
 	if err = presenceConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start presence consumer")
 	}
 
 	keyChangeConsumer := consumers.NewOutputKeyChangeEventConsumer(
-		base.ProcessContext, cfg, cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent),
+		processContext, &dendriteCfg.SyncAPI, dendriteCfg.Global.JetStream.Prefixed(jetstream.OutputKeyChangeEvent),
 		js, rsAPI, syncDB, notifier,
 		streams.DeviceListStreamProvider,
 	)
@@ -85,51 +101,51 @@ func AddPublicRoutes(
 	}
 
 	roomConsumer := consumers.NewOutputRoomEventConsumer(
-		base.ProcessContext, cfg, js, syncDB, notifier, streams.PDUStreamProvider,
-		streams.InviteStreamProvider, rsAPI, base.Fulltext,
+		processContext, &dendriteCfg.SyncAPI, js, syncDB, notifier, streams.PDUStreamProvider,
+		streams.InviteStreamProvider, rsAPI, fts,
 	)
 	if err = roomConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start room server consumer")
 	}
 
 	clientConsumer := consumers.NewOutputClientDataConsumer(
-		base.ProcessContext, cfg, js, natsClient, syncDB, notifier,
-		streams.AccountDataStreamProvider, base.Fulltext,
+		processContext, &dendriteCfg.SyncAPI, js, natsClient, syncDB, notifier,
+		streams.AccountDataStreamProvider, fts,
 	)
 	if err = clientConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start client data consumer")
 	}
 
 	notificationConsumer := consumers.NewOutputNotificationDataConsumer(
-		base.ProcessContext, cfg, js, syncDB, notifier, streams.NotificationDataStreamProvider,
+		processContext, &dendriteCfg.SyncAPI, js, syncDB, notifier, streams.NotificationDataStreamProvider,
 	)
 	if err = notificationConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start notification data consumer")
 	}
 
 	typingConsumer := consumers.NewOutputTypingEventConsumer(
-		base.ProcessContext, cfg, js, eduCache, notifier, streams.TypingStreamProvider,
+		processContext, &dendriteCfg.SyncAPI, js, eduCache, notifier, streams.TypingStreamProvider,
 	)
 	if err = typingConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start typing consumer")
 	}
 
 	sendToDeviceConsumer := consumers.NewOutputSendToDeviceEventConsumer(
-		base.ProcessContext, cfg, js, syncDB, userAPI, notifier, streams.SendToDeviceStreamProvider,
+		processContext, &dendriteCfg.SyncAPI, js, syncDB, userAPI, notifier, streams.SendToDeviceStreamProvider,
 	)
 	if err = sendToDeviceConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start send-to-device consumer")
 	}
 
 	receiptConsumer := consumers.NewOutputReceiptEventConsumer(
-		base.ProcessContext, cfg, js, syncDB, notifier, streams.ReceiptStreamProvider,
+		processContext, &dendriteCfg.SyncAPI, js, syncDB, notifier, streams.ReceiptStreamProvider,
 	)
 	if err = receiptConsumer.Start(); err != nil {
 		logrus.WithError(err).Panicf("failed to start receipts consumer")
 	}
 
 	routing.Setup(
-		base.PublicClientAPIMux, requestPool, syncDB, userAPI,
-		rsAPI, cfg, base.Caches, base.Fulltext,
+		routers.Client, requestPool, syncDB, userAPI,
+		rsAPI, &dendriteCfg.SyncAPI, caches, fts,
 	)
 }
diff --git a/userapi/storage/postgres/storage.go b/userapi/storage/postgres/storage.go
index 673d123b..7bfae7b2 100644
--- a/userapi/storage/postgres/storage.go
+++ b/userapi/storage/postgres/storage.go
@@ -23,7 +23,6 @@ import (
 	"github.com/matrix-org/gomatrixserverlib"
 
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/userapi/storage/postgres/deltas"
 	"github.com/matrix-org/dendrite/userapi/storage/shared"
@@ -33,8 +32,8 @@ import (
 )
 
 // NewDatabase creates a new accounts and profiles database
-func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) {
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter())
+func NewDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) {
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, err
 	}
@@ -51,7 +50,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions,
 			return deltas.UpServerNames(ctx, txn, serverName)
 		},
 	})
-	if err = m.Up(base.Context()); err != nil {
+	if err = m.Up(ctx); err != nil {
 		return nil, err
 	}
 
@@ -111,7 +110,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions,
 			return deltas.UpServerNamesPopulate(ctx, txn, serverName)
 		},
 	})
-	if err = m.Up(base.Context()); err != nil {
+	if err = m.Up(ctx); err != nil {
 		return nil, err
 	}
 
@@ -137,8 +136,8 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions,
 	}, nil
 }
 
-func NewKeyDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.KeyDatabase, error) {
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter())
+func NewKeyDatabase(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (*shared.KeyDatabase, error) {
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, err
 	}
diff --git a/userapi/storage/sqlite3/storage.go b/userapi/storage/sqlite3/storage.go
index 0f3eeed1..3742eeba 100644
--- a/userapi/storage/sqlite3/storage.go
+++ b/userapi/storage/sqlite3/storage.go
@@ -23,7 +23,6 @@ import (
 	"github.com/matrix-org/gomatrixserverlib"
 
 	"github.com/matrix-org/dendrite/internal/sqlutil"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 
 	"github.com/matrix-org/dendrite/userapi/storage/shared"
@@ -31,8 +30,8 @@ import (
 )
 
 // NewUserDatabase creates a new accounts and profiles database
-func NewUserDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) {
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter())
+func NewUserDatabase(ctx context.Context, conMan sqlutil.Connections, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) {
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, err
 	}
@@ -49,7 +48,7 @@ func NewUserDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptio
 			return deltas.UpServerNames(ctx, txn, serverName)
 		},
 	})
-	if err = m.Up(base.Context()); err != nil {
+	if err = m.Up(ctx); err != nil {
 		return nil, err
 	}
 
@@ -109,7 +108,7 @@ func NewUserDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptio
 			return deltas.UpServerNamesPopulate(ctx, txn, serverName)
 		},
 	})
-	if err = m.Up(base.Context()); err != nil {
+	if err = m.Up(ctx); err != nil {
 		return nil, err
 	}
 
@@ -135,8 +134,8 @@ func NewUserDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptio
 	}, nil
 }
 
-func NewKeyDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.KeyDatabase, error) {
-	db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter())
+func NewKeyDatabase(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (*shared.KeyDatabase, error) {
+	db, writer, err := conMan.Connection(dbProperties)
 	if err != nil {
 		return nil, err
 	}
diff --git a/userapi/storage/storage.go b/userapi/storage/storage.go
index 0329fb46..6981765f 100644
--- a/userapi/storage/storage.go
+++ b/userapi/storage/storage.go
@@ -18,12 +18,13 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/userapi/storage/postgres"
 	"github.com/matrix-org/dendrite/userapi/storage/sqlite3"
@@ -32,7 +33,8 @@ import (
 // NewUserDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme)
 // and sets postgres connection parameters
 func NewUserDatabase(
-	base *base.BaseDendrite,
+	ctx context.Context,
+	conMan sqlutil.Connections,
 	dbProperties *config.DatabaseOptions,
 	serverName gomatrixserverlib.ServerName,
 	bcryptCost int,
@@ -42,9 +44,9 @@ func NewUserDatabase(
 ) (UserDatabase, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewUserDatabase(base, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart)
+		return sqlite3.NewUserDatabase(ctx, conMan, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.NewDatabase(base, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart)
+		return postgres.NewDatabase(ctx, conMan, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
@@ -52,12 +54,12 @@ func NewUserDatabase(
 
 // NewKeyDatabase opens a new Postgres or Sqlite database (base on dataSourceName) scheme)
 // and sets postgres connection parameters.
-func NewKeyDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (KeyDatabase, error) {
+func NewKeyDatabase(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (KeyDatabase, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewKeyDatabase(base, dbProperties)
+		return sqlite3.NewKeyDatabase(conMan, dbProperties)
 	case dbProperties.ConnectionString.IsPostgres():
-		return postgres.NewKeyDatabase(base, dbProperties)
+		return postgres.NewKeyDatabase(conMan, dbProperties)
 	default:
 		return nil, fmt.Errorf("unexpected database type")
 	}
diff --git a/userapi/storage/storage_wasm.go b/userapi/storage/storage_wasm.go
index 163e3e17..19e5f23c 100644
--- a/userapi/storage/storage_wasm.go
+++ b/userapi/storage/storage_wasm.go
@@ -15,17 +15,19 @@
 package storage
 
 import (
+	"context"
 	"fmt"
 	"time"
 
-	"github.com/matrix-org/dendrite/setup/base"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/userapi/storage/sqlite3"
 	"github.com/matrix-org/gomatrixserverlib"
 )
 
-func NewUserAPIDatabase(
-	base *base.BaseDendrite,
+func NewUserDatabase(
+	ctx context.Context,
+	conMan sqlutil.Connections,
 	dbProperties *config.DatabaseOptions,
 	serverName gomatrixserverlib.ServerName,
 	bcryptCost int,
@@ -35,7 +37,20 @@ func NewUserAPIDatabase(
 ) (UserDatabase, error) {
 	switch {
 	case dbProperties.ConnectionString.IsSQLite():
-		return sqlite3.NewUserDatabase(base, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart)
+		return sqlite3.NewUserDatabase(ctx, conMan, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart)
+	case dbProperties.ConnectionString.IsPostgres():
+		return nil, fmt.Errorf("can't use Postgres implementation")
+	default:
+		return nil, fmt.Errorf("unexpected database type")
+	}
+}
+
+// NewKeyDatabase opens a new Postgres or Sqlite database (base on dataSourceName) scheme)
+// and sets postgres connection parameters.
+func NewKeyDatabase(conMan sqlutil.Connections, dbProperties *config.DatabaseOptions) (KeyDatabase, error) {
+	switch {
+	case dbProperties.ConnectionString.IsSQLite():
+		return sqlite3.NewKeyDatabase(conMan, dbProperties)
 	case dbProperties.ConnectionString.IsPostgres():
 		return nil, fmt.Errorf("can't use Postgres implementation")
 	default:
diff --git a/userapi/userapi.go b/userapi/userapi.go
index 826bd721..6dcbc121 100644
--- a/userapi/userapi.go
+++ b/userapi/userapi.go
@@ -18,10 +18,13 @@ import (
 	"time"
 
 	fedsenderapi "github.com/matrix-org/dendrite/federationapi/api"
+	"github.com/matrix-org/dendrite/internal/pushgateway"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/sirupsen/logrus"
 
 	rsapi "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/dendrite/userapi/consumers"
@@ -34,30 +37,33 @@ import (
 // NewInternalAPI returns a concrete implementation of the internal API. Callers
 // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
 func NewInternalAPI(
-	base *base.BaseDendrite,
+	processContext *process.ProcessContext,
+	dendriteCfg *config.Dendrite,
+	cm sqlutil.Connections,
+	natsInstance *jetstream.NATSInstance,
 	rsAPI rsapi.UserRoomserverAPI,
 	fedClient fedsenderapi.KeyserverFederationAPI,
 ) *internal.UserInternalAPI {
-	cfg := &base.Cfg.UserAPI
-	js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
-	appServices := base.Cfg.Derived.ApplicationServices
+	js, _ := natsInstance.Prepare(processContext, &dendriteCfg.Global.JetStream)
+	appServices := dendriteCfg.Derived.ApplicationServices
 
-	pgClient := base.PushGatewayHTTPClient()
+	pgClient := pushgateway.NewHTTPClient(dendriteCfg.UserAPI.PushGatewayDisableTLSValidation)
 
 	db, err := storage.NewUserDatabase(
-		base,
-		&cfg.AccountDatabase,
-		cfg.Matrix.ServerName,
-		cfg.BCryptCost,
-		cfg.OpenIDTokenLifetimeMS,
+		processContext.Context(),
+		cm,
+		&dendriteCfg.UserAPI.AccountDatabase,
+		dendriteCfg.Global.ServerName,
+		dendriteCfg.UserAPI.BCryptCost,
+		dendriteCfg.UserAPI.OpenIDTokenLifetimeMS,
 		api.DefaultLoginTokenLifetime,
-		cfg.Matrix.ServerNotices.LocalPart,
+		dendriteCfg.UserAPI.Matrix.ServerNotices.LocalPart,
 	)
 	if err != nil {
 		logrus.WithError(err).Panicf("failed to connect to accounts db")
 	}
 
-	keyDB, err := storage.NewKeyDatabase(base, &base.Cfg.KeyServer.Database)
+	keyDB, err := storage.NewKeyDatabase(cm, &dendriteCfg.KeyServer.Database)
 	if err != nil {
 		logrus.WithError(err).Panicf("failed to connect to key db")
 	}
@@ -68,11 +74,11 @@ func NewInternalAPI(
 		// it's handled by clientapi, and hence uses its topic. When user
 		// API handles it for all account data, we can remove it from
 		// here.
-		cfg.Matrix.JetStream.Prefixed(jetstream.OutputClientData),
-		cfg.Matrix.JetStream.Prefixed(jetstream.OutputNotificationData),
+		dendriteCfg.Global.JetStream.Prefixed(jetstream.OutputClientData),
+		dendriteCfg.Global.JetStream.Prefixed(jetstream.OutputNotificationData),
 	)
 	keyChangeProducer := &producers.KeyChange{
-		Topic:     cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent),
+		Topic:     dendriteCfg.Global.JetStream.Prefixed(jetstream.OutputKeyChangeEvent),
 		JetStream: js,
 		DB:        keyDB,
 	}
@@ -82,15 +88,15 @@ func NewInternalAPI(
 		KeyDatabase:          keyDB,
 		SyncProducer:         syncProducer,
 		KeyChangeProducer:    keyChangeProducer,
-		Config:               cfg,
+		Config:               &dendriteCfg.UserAPI,
 		AppServices:          appServices,
 		RSAPI:                rsAPI,
-		DisableTLSValidation: cfg.PushGatewayDisableTLSValidation,
+		DisableTLSValidation: dendriteCfg.UserAPI.PushGatewayDisableTLSValidation,
 		PgClient:             pgClient,
 		FedClient:            fedClient,
 	}
 
-	updater := internal.NewDeviceListUpdater(base.ProcessContext, keyDB, userAPI, keyChangeProducer, fedClient, 8, rsAPI, cfg.Matrix.ServerName) // 8 workers TODO: configurable
+	updater := internal.NewDeviceListUpdater(processContext, keyDB, userAPI, keyChangeProducer, fedClient, 8, rsAPI, dendriteCfg.Global.ServerName) // 8 workers TODO: configurable
 	userAPI.Updater = updater
 	// Remove users which we don't share a room with anymore
 	if err := updater.CleanUp(); err != nil {
@@ -104,28 +110,28 @@ func NewInternalAPI(
 	}()
 
 	dlConsumer := consumers.NewDeviceListUpdateConsumer(
-		base.ProcessContext, cfg, js, updater,
+		processContext, &dendriteCfg.UserAPI, js, updater,
 	)
 	if err := dlConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start device list consumer")
 	}
 
 	sigConsumer := consumers.NewSigningKeyUpdateConsumer(
-		base.ProcessContext, cfg, js, userAPI,
+		processContext, &dendriteCfg.UserAPI, js, userAPI,
 	)
 	if err := sigConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start signing key consumer")
 	}
 
 	receiptConsumer := consumers.NewOutputReceiptEventConsumer(
-		base.ProcessContext, cfg, js, db, syncProducer, pgClient,
+		processContext, &dendriteCfg.UserAPI, js, db, syncProducer, pgClient,
 	)
 	if err := receiptConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start user API receipt consumer")
 	}
 
 	eventConsumer := consumers.NewOutputRoomEventConsumer(
-		base.ProcessContext, cfg, js, db, pgClient, rsAPI, syncProducer,
+		processContext, &dendriteCfg.UserAPI, js, db, pgClient, rsAPI, syncProducer,
 	)
 	if err := eventConsumer.Start(); err != nil {
 		logrus.WithError(err).Panic("failed to start user API streamed event consumer")
@@ -134,15 +140,15 @@ func NewInternalAPI(
 	var cleanOldNotifs func()
 	cleanOldNotifs = func() {
 		logrus.Infof("Cleaning old notifications")
-		if err := db.DeleteOldNotifications(base.Context()); err != nil {
+		if err := db.DeleteOldNotifications(processContext.Context()); err != nil {
 			logrus.WithError(err).Error("Failed to clean old notifications")
 		}
 		time.AfterFunc(time.Hour, cleanOldNotifs)
 	}
 	time.AfterFunc(time.Minute, cleanOldNotifs)
 
-	if base.Cfg.Global.ReportStats.Enabled {
-		go util.StartPhoneHomeCollector(time.Now(), base.Cfg, db)
+	if dendriteCfg.Global.ReportStats.Enabled {
+		go util.StartPhoneHomeCollector(time.Now(), dendriteCfg, db)
 	}
 
 	return userAPI


Test patch:
diff --git a/appservice/appservice_test.go b/appservice/appservice_test.go
index de9f5aaf..6c8a07b5 100644
--- a/appservice/appservice_test.go
+++ b/appservice/appservice_test.go
@@ -9,11 +9,15 @@ import (
 	"regexp"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/matrix-org/dendrite/appservice"
 	"github.com/matrix-org/dendrite/appservice/api"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/userapi"
 
@@ -104,11 +108,11 @@ func TestAppserviceInternalAPI(t *testing.T) {
 	}
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, closeBase := testrig.CreateBaseDendrite(t, dbType)
-		defer closeBase()
+		cfg, ctx, close := testrig.CreateConfig(t, dbType)
+		defer close()
 
 		// Create a dummy application service
-		base.Cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{
+		cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{
 			{
 				ID:              "someID",
 				URL:             srv.URL,
@@ -123,10 +127,17 @@ func TestAppserviceInternalAPI(t *testing.T) {
 			},
 		}
 
+		t.Cleanup(func() {
+			ctx.ShutdownDendrite()
+			ctx.WaitForShutdown()
+		})
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 		// Create required internal APIs
-		rsAPI := roomserver.NewInternalAPI(base)
-		usrAPI := userapi.NewInternalAPI(base, rsAPI, nil)
-		asAPI := appservice.NewInternalAPI(base, usrAPI, rsAPI)
+		natsInstance := jetstream.NATSInstance{}
+		cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
+		rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil)
+		asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
 
 		runCases(t, asAPI)
 	})
diff --git a/clientapi/admin_test.go b/clientapi/admin_test.go
index 300d3a88..4d2bf67b 100644
--- a/clientapi/admin_test.go
+++ b/clientapi/admin_test.go
@@ -5,12 +5,17 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"testing"
+	"time"
 
 	"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
 	"github.com/matrix-org/dendrite/federationapi"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/roomserver/api"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/syncapi"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -29,19 +34,22 @@ func TestAdminResetPassword(t *testing.T) {
 
 	ctx := context.Background()
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
-
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
+		natsInstance := jetstream.NATSInstance{}
 		// add a vhost
-		base.Cfg.Global.VirtualHosts = append(base.Cfg.Global.VirtualHosts, &config.VirtualHost{
+		cfg.Global.VirtualHosts = append(cfg.Global.VirtualHosts, &config.VirtualHost{
 			SigningIdentity: gomatrixserverlib.SigningIdentity{ServerName: "vh1"},
 		})
 
-		rsAPI := roomserver.NewInternalAPI(base)
+		routers := httputil.NewRouters()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 		// Needed for changing the password/login
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 		// We mostly need the userAPI for this test, so nil for other APIs/caches etc.
-		AddPublicRoutes(base, nil, rsAPI, nil, nil, nil, userAPI, nil, nil)
+		AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
 
 		// Create the users in the userapi and login
 		accessTokens := map[*test.User]string{
@@ -71,7 +79,7 @@ func TestAdminResetPassword(t *testing.T) {
 				"password": password,
 			}))
 			rec := httptest.NewRecorder()
-			base.PublicClientAPIMux.ServeHTTP(rec, req)
+			routers.Client.ServeHTTP(rec, req)
 			if rec.Code != http.StatusOK {
 				t.Fatalf("failed to login: %s", rec.Body.String())
 			}
@@ -124,7 +132,7 @@ func TestAdminResetPassword(t *testing.T) {
 				}
 
 				rec := httptest.NewRecorder()
-				base.DendriteAdminMux.ServeHTTP(rec, req)
+				routers.DendriteAdmin.ServeHTTP(rec, req)
 				t.Logf("%s", rec.Body.String())
 				if tc.wantOK && rec.Code != http.StatusOK {
 					t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
@@ -147,16 +155,19 @@ func TestPurgeRoom(t *testing.T) {
 	ctx := context.Background()
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		natsInstance := jetstream.NATSInstance{}
+		defer close()
 
-		fedClient := base.CreateFederationClient()
-		rsAPI := roomserver.NewInternalAPI(base)
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		routers := httputil.NewRouters()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 
 		// this starts the JetStream consumers
-		syncapi.AddPublicRoutes(base, userAPI, rsAPI)
-		federationapi.NewInternalAPI(base, fedClient, rsAPI, base.Caches, nil, true)
+		syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, userAPI, rsAPI, caches, caching.DisableMetrics)
+		federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, nil, rsAPI, caches, nil, true)
 		rsAPI.SetFederationAPI(nil, nil)
 
 		// Create the room
@@ -165,7 +176,7 @@ func TestPurgeRoom(t *testing.T) {
 		}
 
 		// We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
-		AddPublicRoutes(base, nil, rsAPI, nil, nil, nil, userAPI, nil, nil)
+		AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
 
 		// Create the users in the userapi and login
 		accessTokens := map[*test.User]string{
@@ -193,7 +204,7 @@ func TestPurgeRoom(t *testing.T) {
 				"password": password,
 			}))
 			rec := httptest.NewRecorder()
-			base.PublicClientAPIMux.ServeHTTP(rec, req)
+			routers.Client.ServeHTTP(rec, req)
 			if rec.Code != http.StatusOK {
 				t.Fatalf("failed to login: %s", rec.Body.String())
 			}
@@ -218,7 +229,7 @@ func TestPurgeRoom(t *testing.T) {
 				req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin])
 
 				rec := httptest.NewRecorder()
-				base.DendriteAdminMux.ServeHTTP(rec, req)
+				routers.DendriteAdmin.ServeHTTP(rec, req)
 				t.Logf("%s", rec.Body.String())
 				if tc.wantOK && rec.Code != http.StatusOK {
 					t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
diff --git a/clientapi/routing/auth_fallback_test.go b/clientapi/routing/auth_fallback_test.go
index 534581bd..afeca051 100644
--- a/clientapi/routing/auth_fallback_test.go
+++ b/clientapi/routing/auth_fallback_test.go
@@ -10,30 +10,28 @@ import (
 
 	"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
 	"github.com/matrix-org/dendrite/setup/config"
-	"github.com/matrix-org/dendrite/test/testrig"
 )
 
 func Test_AuthFallback(t *testing.T) {
-	base, _, _ := testrig.Base(nil)
-	defer base.Close()
-
+	cfg := config.Dendrite{}
+	cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
 	for _, useHCaptcha := range []bool{false, true} {
 		for _, recaptchaEnabled := range []bool{false, true} {
 			for _, wantErr := range []bool{false, true} {
 				t.Run(fmt.Sprintf("useHCaptcha(%v) - recaptchaEnabled(%v) - wantErr(%v)", useHCaptcha, recaptchaEnabled, wantErr), func(t *testing.T) {
 					// Set the defaults for each test
-					base.Cfg.ClientAPI.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
-					base.Cfg.ClientAPI.RecaptchaEnabled = recaptchaEnabled
-					base.Cfg.ClientAPI.RecaptchaPublicKey = "pub"
-					base.Cfg.ClientAPI.RecaptchaPrivateKey = "priv"
+					cfg.ClientAPI.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
+					cfg.ClientAPI.RecaptchaEnabled = recaptchaEnabled
+					cfg.ClientAPI.RecaptchaPublicKey = "pub"
+					cfg.ClientAPI.RecaptchaPrivateKey = "priv"
 					if useHCaptcha {
-						base.Cfg.ClientAPI.RecaptchaSiteVerifyAPI = "https://hcaptcha.com/siteverify"
-						base.Cfg.ClientAPI.RecaptchaApiJsUrl = "https://js.hcaptcha.com/1/api.js"
-						base.Cfg.ClientAPI.RecaptchaFormField = "h-captcha-response"
-						base.Cfg.ClientAPI.RecaptchaSitekeyClass = "h-captcha"
+						cfg.ClientAPI.RecaptchaSiteVerifyAPI = "https://hcaptcha.com/siteverify"
+						cfg.ClientAPI.RecaptchaApiJsUrl = "https://js.hcaptcha.com/1/api.js"
+						cfg.ClientAPI.RecaptchaFormField = "h-captcha-response"
+						cfg.ClientAPI.RecaptchaSitekeyClass = "h-captcha"
 					}
 					cfgErrs := &config.ConfigErrors{}
-					base.Cfg.ClientAPI.Verify(cfgErrs)
+					cfg.ClientAPI.Verify(cfgErrs)
 					if len(*cfgErrs) > 0 {
 						t.Fatalf("(hCaptcha=%v) unexpected config errors: %s", useHCaptcha, cfgErrs.Error())
 					}
@@ -41,7 +39,7 @@ func Test_AuthFallback(t *testing.T) {
 					req := httptest.NewRequest(http.MethodGet, "/?session=1337", nil)
 					rec := httptest.NewRecorder()
 
-					AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
+					AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
 					if !recaptchaEnabled {
 						if rec.Code != http.StatusBadRequest {
 							t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
@@ -50,8 +48,8 @@ func Test_AuthFallback(t *testing.T) {
 							t.Fatalf("unexpected response body: %s", rec.Body.String())
 						}
 					} else {
-						if !strings.Contains(rec.Body.String(), base.Cfg.ClientAPI.RecaptchaSitekeyClass) {
-							t.Fatalf("body does not contain %s: %s", base.Cfg.ClientAPI.RecaptchaSitekeyClass, rec.Body.String())
+						if !strings.Contains(rec.Body.String(), cfg.ClientAPI.RecaptchaSitekeyClass) {
+							t.Fatalf("body does not contain %s: %s", cfg.ClientAPI.RecaptchaSitekeyClass, rec.Body.String())
 						}
 					}
 
@@ -64,14 +62,14 @@ func Test_AuthFallback(t *testing.T) {
 					}))
 					defer srv.Close() // nolint: errcheck
 
-					base.Cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
+					cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
 
 					// check the result after sending the captcha
 					req = httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
 					req.Form = url.Values{}
-					req.Form.Add(base.Cfg.ClientAPI.RecaptchaFormField, "someRandomValue")
+					req.Form.Add(cfg.ClientAPI.RecaptchaFormField, "someRandomValue")
 					rec = httptest.NewRecorder()
-					AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
+					AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
 					if recaptchaEnabled {
 						if !wantErr {
 							if rec.Code != http.StatusOK {
@@ -105,7 +103,7 @@ func Test_AuthFallback(t *testing.T) {
 	t.Run("unknown fallbacks are handled correctly", func(t *testing.T) {
 		req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
 		rec := httptest.NewRecorder()
-		AuthFallback(rec, req, "DoesNotExist", &base.Cfg.ClientAPI)
+		AuthFallback(rec, req, "DoesNotExist", &cfg.ClientAPI)
 		if rec.Code != http.StatusNotImplemented {
 			t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusNotImplemented)
 		}
@@ -114,7 +112,7 @@ func Test_AuthFallback(t *testing.T) {
 	t.Run("unknown methods are handled correctly", func(t *testing.T) {
 		req := httptest.NewRequest(http.MethodDelete, "/?session=1337", nil)
 		rec := httptest.NewRecorder()
-		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
+		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
 		if rec.Code != http.StatusMethodNotAllowed {
 			t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusMethodNotAllowed)
 		}
@@ -123,7 +121,7 @@ func Test_AuthFallback(t *testing.T) {
 	t.Run("missing session parameter is handled correctly", func(t *testing.T) {
 		req := httptest.NewRequest(http.MethodGet, "/", nil)
 		rec := httptest.NewRecorder()
-		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
+		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
 		if rec.Code != http.StatusBadRequest {
 			t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
 		}
@@ -132,7 +130,7 @@ func Test_AuthFallback(t *testing.T) {
 	t.Run("missing session parameter is handled correctly", func(t *testing.T) {
 		req := httptest.NewRequest(http.MethodGet, "/", nil)
 		rec := httptest.NewRecorder()
-		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
+		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
 		if rec.Code != http.StatusBadRequest {
 			t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
 		}
@@ -141,7 +139,7 @@ func Test_AuthFallback(t *testing.T) {
 	t.Run("missing 'response' is handled correctly", func(t *testing.T) {
 		req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
 		rec := httptest.NewRecorder()
-		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &base.Cfg.ClientAPI)
+		AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
 		if rec.Code != http.StatusBadRequest {
 			t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
 		}
diff --git a/clientapi/routing/joinroom_test.go b/clientapi/routing/joinroom_test.go
index 1450ef4b..fd58ff5d 100644
--- a/clientapi/routing/joinroom_test.go
+++ b/clientapi/routing/joinroom_test.go
@@ -7,6 +7,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/gomatrixserverlib"
 
 	"github.com/matrix-org/dendrite/appservice"
@@ -24,12 +27,15 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
 
 	ctx := context.Background()
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
 
-		rsAPI := roomserver.NewInternalAPI(base)
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
-		asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		natsInstance := jetstream.NATSInstance{}
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
+		asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
 		rsAPI.SetFederationAPI(nil, nil) // creates the rs.Inputer etc
 
 		// Create the users in the userapi
@@ -61,7 +67,7 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
 			RoomAliasName: "alias",
 			Invite:        []string{bob.ID},
 			GuestCanJoin:  false,
-		}, aliceDev, &base.Cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
+		}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
 		crResp, ok := resp.JSON.(createRoomResponse)
 		if !ok {
 			t.Fatalf("response is not a createRoomResponse: %+v", resp)
@@ -76,7 +82,7 @@ func TestJoinRoomByIDOrAlias(t *testing.T) {
 			Preset:       presetPublicChat,
 			Invite:       []string{charlie.ID},
 			GuestCanJoin: true,
-		}, aliceDev, &base.Cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
+		}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
 		crRespWithGuestAccess, ok := resp.JSON.(createRoomResponse)
 		if !ok {
 			t.Fatalf("response is not a createRoomResponse: %+v", resp)
diff --git a/clientapi/routing/login_test.go b/clientapi/routing/login_test.go
index b72db9d8..b2773076 100644
--- a/clientapi/routing/login_test.go
+++ b/clientapi/routing/login_test.go
@@ -7,10 +7,15 @@ import (
 	"net/http/httptest"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
 
@@ -28,20 +33,24 @@ func TestLogin(t *testing.T) {
 
 	ctx := context.Background()
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
-		base.Cfg.ClientAPI.RateLimiting.Enabled = false
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
+		cfg.ClientAPI.RateLimiting.Enabled = false
+		natsInstance := jetstream.NATSInstance{}
 		// add a vhost
-		base.Cfg.Global.VirtualHosts = append(base.Cfg.Global.VirtualHosts, &config.VirtualHost{
+		cfg.Global.VirtualHosts = append(cfg.Global.VirtualHosts, &config.VirtualHost{
 			SigningIdentity: gomatrixserverlib.SigningIdentity{ServerName: "vh1"},
 		})
 
-		rsAPI := roomserver.NewInternalAPI(base)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		routers := httputil.NewRouters()
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 		// Needed for /login
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 
 		// We mostly need the userAPI for this test, so nil for other APIs/caches etc.
-		Setup(base, &base.Cfg.ClientAPI, nil, nil, userAPI, nil, nil, nil, nil, nil, nil, &base.Cfg.MSCs, nil)
+		Setup(routers, cfg, nil, nil, userAPI, nil, nil, nil, nil, nil, nil, nil, caching.DisableMetrics)
 
 		// Create password
 		password := util.RandomString(8)
@@ -114,7 +123,7 @@ func TestLogin(t *testing.T) {
 					"password": password,
 				}))
 				rec := httptest.NewRecorder()
-				base.PublicClientAPIMux.ServeHTTP(rec, req)
+				routers.Client.ServeHTTP(rec, req)
 				if tc.wantOK && rec.Code != http.StatusOK {
 					t.Fatalf("failed to login: %s", rec.Body.String())
 				}
diff --git a/clientapi/routing/register_test.go b/clientapi/routing/register_test.go
index 651e3d3d..46cd8b2b 100644
--- a/clientapi/routing/register_test.go
+++ b/clientapi/routing/register_test.go
@@ -30,8 +30,11 @@ import (
 	"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
 	"github.com/matrix-org/dendrite/clientapi/jsonerror"
 	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/dendrite/userapi"
@@ -404,11 +407,15 @@ func Test_register(t *testing.T) {
 	}
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
 
-		rsAPI := roomserver.NewInternalAPI(base)
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		natsInstance := jetstream.NATSInstance{}
+
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 
 		for _, tc := range testCases {
 			t.Run(tc.name, func(t *testing.T) {
@@ -430,16 +437,16 @@ func Test_register(t *testing.T) {
 						}
 					}))
 					defer srv.Close()
-					base.Cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
+					cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
 				}
 
-				if err := base.Cfg.Derive(); err != nil {
+				if err := cfg.Derive(); err != nil {
 					t.Fatalf("failed to derive config: %s", err)
 				}
 
-				base.Cfg.ClientAPI.RecaptchaEnabled = tc.enableRecaptcha
-				base.Cfg.ClientAPI.RegistrationDisabled = tc.registrationDisabled
-				base.Cfg.ClientAPI.GuestsDisabled = tc.guestsDisabled
+				cfg.ClientAPI.RecaptchaEnabled = tc.enableRecaptcha
+				cfg.ClientAPI.RegistrationDisabled = tc.registrationDisabled
+				cfg.ClientAPI.GuestsDisabled = tc.guestsDisabled
 
 				if tc.kind == "" {
 					tc.kind = "user"
@@ -467,15 +474,15 @@ func Test_register(t *testing.T) {
 
 				req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/?kind=%s", tc.kind), body)
 
-				resp := Register(req, userAPI, &base.Cfg.ClientAPI)
+				resp := Register(req, userAPI, &cfg.ClientAPI)
 				t.Logf("Resp: %+v", resp)
 
 				// The first request should return a userInteractiveResponse
 				switch r := resp.JSON.(type) {
 				case userInteractiveResponse:
 					// Check that the flows are the ones we configured
-					if !reflect.DeepEqual(r.Flows, base.Cfg.Derived.Registration.Flows) {
-						t.Fatalf("unexpected registration flows: %+v, want %+v", r.Flows, base.Cfg.Derived.Registration.Flows)
+					if !reflect.DeepEqual(r.Flows, cfg.Derived.Registration.Flows) {
+						t.Fatalf("unexpected registration flows: %+v, want %+v", r.Flows, cfg.Derived.Registration.Flows)
 					}
 				case *jsonerror.MatrixError:
 					if !reflect.DeepEqual(tc.wantResponse, resp) {
@@ -531,7 +538,7 @@ func Test_register(t *testing.T) {
 
 				req = httptest.NewRequest(http.MethodPost, "/", body)
 
-				resp = Register(req, userAPI, &base.Cfg.ClientAPI)
+				resp = Register(req, userAPI, &cfg.ClientAPI)
 
 				switch resp.JSON.(type) {
 				case *jsonerror.MatrixError:
@@ -574,16 +581,19 @@ func Test_register(t *testing.T) {
 
 func TestRegisterUserWithDisplayName(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
-		base.Cfg.Global.ServerName = "server"
-
-		rsAPI := roomserver.NewInternalAPI(base)
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
+		cfg.Global.ServerName = "server"
+
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		natsInstance := jetstream.NATSInstance{}
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 		deviceName, deviceID := "deviceName", "deviceID"
 		expectedDisplayName := "DisplayName"
 		response := completeRegistration(
-			base.Context(),
+			processCtx.Context(),
 			userAPI,
 			"user",
 			"server",
@@ -603,7 +613,7 @@ func TestRegisterUserWithDisplayName(t *testing.T) {
 
 		req := api.QueryProfileRequest{UserID: "@user:server"}
 		var res api.QueryProfileResponse
-		err := userAPI.QueryProfile(base.Context(), &req, &res)
+		err := userAPI.QueryProfile(processCtx.Context(), &req, &res)
 		assert.NoError(t, err)
 		assert.Equal(t, expectedDisplayName, res.DisplayName)
 	})
@@ -611,14 +621,17 @@ func TestRegisterUserWithDisplayName(t *testing.T) {
 
 func TestRegisterAdminUsingSharedSecret(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-		defer baseClose()
-		base.Cfg.Global.ServerName = "server"
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
+		natsInstance := jetstream.NATSInstance{}
+		cfg.Global.ServerName = "server"
 		sharedSecret := "dendritetest"
-		base.Cfg.ClientAPI.RegistrationSharedSecret = sharedSecret
+		cfg.ClientAPI.RegistrationSharedSecret = sharedSecret
 
-		rsAPI := roomserver.NewInternalAPI(base)
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 
 		expectedDisplayName := "rabbit"
 		jsonStr := []byte(`{"admin":true,"mac":"24dca3bba410e43fe64b9b5c28306693bf3baa9f","nonce":"759f047f312b99ff428b21d581256f8592b8976e58bc1b543972dc6147e529a79657605b52d7becd160ff5137f3de11975684319187e06901955f79e5a6c5a79","password":"wonderland","username":"alice","displayname":"rabbit"}`)
@@ -642,7 +655,7 @@ func TestRegisterAdminUsingSharedSecret(t *testing.T) {
 		ssrr := httptest.NewRequest(http.MethodPost, "/", body)
 
 		response := handleSharedSecretRegistration(
-			&base.Cfg.ClientAPI,
+			&cfg.ClientAPI,
 			userAPI,
 			r,
 			ssrr,
@@ -651,7 +664,7 @@ func TestRegisterAdminUsingSharedSecret(t *testing.T) {
 
 		profilReq := api.QueryProfileRequest{UserID: "@alice:server"}
 		var profileRes api.QueryProfileResponse
-		err = userAPI.QueryProfile(base.Context(), &profilReq, &profileRes)
+		err = userAPI.QueryProfile(processCtx.Context(), &profilReq, &profileRes)
 		assert.NoError(t, err)
 		assert.Equal(t, expectedDisplayName, profileRes.DisplayName)
 	})
diff --git a/cmd/dendrite-upgrade-tests/main.go b/cmd/dendrite-upgrade-tests/main.go
index 174a80a3..6a0e2179 100644
--- a/cmd/dendrite-upgrade-tests/main.go
+++ b/cmd/dendrite-upgrade-tests/main.go
@@ -259,10 +259,20 @@ func buildDendrite(httpClient *http.Client, dockerClient *client.Client, tmpDir
 
 func getAndSortVersionsFromGithub(httpClient *http.Client) (semVers []*semver.Version, err error) {
 	u := "https://api.github.com/repos/matrix-org/dendrite/tags"
-	res, err := httpClient.Get(u)
-	if err != nil {
-		return nil, err
+
+	var res *http.Response
+	for i := 0; i < 3; i++ {
+		res, err = httpClient.Get(u)
+		if err != nil {
+			return nil, err
+		}
+		if res.StatusCode == 200 {
+			break
+		}
+		log.Printf("Github API returned HTTP %d, retrying\n", res.StatusCode)
+		time.Sleep(time.Second * 5)
 	}
+
 	if res.StatusCode != 200 {
 		return nil, fmt.Errorf("%s returned HTTP %d", u, res.StatusCode)
 	}
diff --git a/docs/development/sytest.md b/docs/development/sytest.md
index 3cfb99e6..4fae2ea3 100644
--- a/docs/development/sytest.md
+++ b/docs/development/sytest.md
@@ -61,7 +61,6 @@ When debugging, the following Docker `run` options may also be useful:
 * `-e "DENDRITE_TRACE_HTTP=1"`: Adds HTTP tracing to server logs.
 * `-e "DENDRITE_TRACE_INTERNAL=1"`: Adds roomserver internal API tracing to
   server logs.
-* `-e "DENDRITE_TRACE_SQL=1"`: Adds tracing to all SQL statements to server logs.
 
 The docker command also supports a single positional argument for the test file to
 run, so you can run a single `.pl` file rather than the whole test suite. For example:
diff --git a/federationapi/federationapi_keys_test.go b/federationapi/federationapi_keys_test.go
index bb6ee893..425cdbb9 100644
--- a/federationapi/federationapi_keys_test.go
+++ b/federationapi/federationapi_keys_test.go
@@ -12,12 +12,14 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/gomatrixserverlib"
 
 	"github.com/matrix-org/dendrite/federationapi/api"
 	"github.com/matrix-org/dendrite/federationapi/routing"
 	"github.com/matrix-org/dendrite/internal/caching"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
@@ -65,7 +67,7 @@ func TestMain(m *testing.M) {
 
 			// Create a new cache but don't enable prometheus!
 			s.cache = caching.NewRistrettoCache(8*1024*1024, time.Hour, false)
-
+			natsInstance := jetstream.NATSInstance{}
 			// Create a temporary directory for JetStream.
 			d, err := os.MkdirTemp("./", "jetstream*")
 			if err != nil {
@@ -109,8 +111,9 @@ func TestMain(m *testing.M) {
 			)
 
 			// Finally, build the server key APIs.
-			sbase := base.NewBaseDendrite(cfg, base.DisableMetrics)
-			s.api = NewInternalAPI(sbase, s.fedclient, nil, s.cache, nil, true)
+			processCtx := process.NewProcessContext()
+			cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+			s.api = NewInternalAPI(processCtx, cfg, cm, &natsInstance, s.fedclient, nil, s.cache, nil, true)
 		}
 
 		// Now that we have built our server key APIs, start the
diff --git a/federationapi/federationapi_test.go b/federationapi/federationapi_test.go
index 57d4b964..d39f4051 100644
--- a/federationapi/federationapi_test.go
+++ b/federationapi/federationapi_test.go
@@ -10,6 +10,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrix"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/nats-io/nats.go"
@@ -18,8 +21,6 @@ import (
 	"github.com/matrix-org/dendrite/federationapi/api"
 	"github.com/matrix-org/dendrite/federationapi/internal"
 	rsapi "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
-	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
@@ -162,21 +163,24 @@ func TestFederationAPIJoinThenKeyUpdate(t *testing.T) {
 }
 
 func testFederationAPIJoinThenKeyUpdate(t *testing.T, dbType test.DBType) {
-	base, close := testrig.CreateBaseDendrite(t, dbType)
-	base.Cfg.FederationAPI.PreferDirectFetch = true
-	base.Cfg.FederationAPI.KeyPerspectives = nil
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+	natsInstance := jetstream.NATSInstance{}
+	cfg.FederationAPI.PreferDirectFetch = true
+	cfg.FederationAPI.KeyPerspectives = nil
 	defer close()
-	jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-	defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
+	jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+	defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
 
 	serverA := gomatrixserverlib.ServerName("server.a")
 	serverAKeyID := gomatrixserverlib.KeyID("ed25519:servera")
 	serverAPrivKey := test.PrivateKeyA
 	creator := test.NewUser(t, test.WithSigningServer(serverA, serverAKeyID, serverAPrivKey))
 
-	myServer := base.Cfg.Global.ServerName
-	myServerKeyID := base.Cfg.Global.KeyID
-	myServerPrivKey := base.Cfg.Global.PrivateKey
+	myServer := cfg.Global.ServerName
+	myServerKeyID := cfg.Global.KeyID
+	myServerPrivKey := cfg.Global.PrivateKey
 	joiningUser := test.NewUser(t, test.WithSigningServer(myServer, myServerKeyID, myServerPrivKey))
 	fmt.Printf("creator: %v joining user: %v\n", creator.ID, joiningUser.ID)
 	room := test.NewRoom(t, creator)
@@ -212,7 +216,7 @@ func testFederationAPIJoinThenKeyUpdate(t *testing.T, dbType test.DBType) {
 			},
 		},
 	}
-	fsapi := federationapi.NewInternalAPI(base, fc, rsapi, base.Caches, nil, false)
+	fsapi := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, fc, rsapi, caches, nil, false)
 
 	var resp api.PerformJoinResponse
 	fsapi.PerformJoin(context.Background(), &api.PerformJoinRequest{
@@ -245,7 +249,7 @@ func testFederationAPIJoinThenKeyUpdate(t *testing.T, dbType test.DBType) {
 	}
 
 	msg := &nats.Msg{
-		Subject: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputKeyChangeEvent),
+		Subject: cfg.Global.JetStream.Prefixed(jetstream.OutputKeyChangeEvent),
 		Header:  nats.Header{},
 		Data:    b,
 	}
@@ -263,30 +267,6 @@ func testFederationAPIJoinThenKeyUpdate(t *testing.T, dbType test.DBType) {
 // Tests that event IDs with '/' in them (escaped as %2F) are correctly passed to the right handler and don't 404.
 // Relevant for v3 rooms and a cause of flakey sytests as the IDs are randomly generated.
 func TestRoomsV3URLEscapeDoNot404(t *testing.T) {
-	_, privKey, _ := ed25519.GenerateKey(nil)
-	cfg := &config.Dendrite{}
-	cfg.Defaults(config.DefaultOpts{
-		Generate:       true,
-		SingleDatabase: false,
-	})
-	cfg.Global.KeyID = gomatrixserverlib.KeyID("ed25519:auto")
-	cfg.Global.ServerName = gomatrixserverlib.ServerName("localhost")
-	cfg.Global.PrivateKey = privKey
-	cfg.Global.JetStream.InMemory = true
-	b := base.NewBaseDendrite(cfg, base.DisableMetrics)
-	keyRing := &test.NopJSONVerifier{}
-	// TODO: This is pretty fragile, as if anything calls anything on these nils this test will break.
-	// Unfortunately, it makes little sense to instantiate these dependencies when we just want to test routing.
-	federationapi.AddPublicRoutes(b, nil, nil, keyRing, nil, &internal.FederationInternalAPI{}, nil)
-	baseURL, cancel := test.ListenAndServe(t, b.PublicFederationAPIMux, true)
-	defer cancel()
-	serverName := gomatrixserverlib.ServerName(strings.TrimPrefix(baseURL, "https://"))
-
-	fedCli := gomatrixserverlib.NewFederationClient(
-		cfg.Global.SigningIdentities(),
-		gomatrixserverlib.WithSkipVerify(true),
-	)
-
 	testCases := []struct {
 		roomVer   gomatrixserverlib.RoomVersion
 		eventJSON string
@@ -315,6 +295,29 @@ func TestRoomsV3URLEscapeDoNot404(t *testing.T) {
 		},
 	}
 
+	cfg, processCtx, close := testrig.CreateConfig(t, test.DBTypeSQLite)
+	defer close()
+	routers := httputil.NewRouters()
+
+	_, privKey, _ := ed25519.GenerateKey(nil)
+	cfg.Global.KeyID = gomatrixserverlib.KeyID("ed25519:auto")
+	cfg.Global.ServerName = gomatrixserverlib.ServerName("localhost")
+	cfg.Global.PrivateKey = privKey
+	cfg.Global.JetStream.InMemory = true
+	keyRing := &test.NopJSONVerifier{}
+	natsInstance := jetstream.NATSInstance{}
+	// TODO: This is pretty fragile, as if anything calls anything on these nils this test will break.
+	// Unfortunately, it makes little sense to instantiate these dependencies when we just want to test routing.
+	federationapi.AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, nil, keyRing, nil, &internal.FederationInternalAPI{}, nil, caching.DisableMetrics)
+	baseURL, cancel := test.ListenAndServe(t, routers.Federation, true)
+	defer cancel()
+	serverName := gomatrixserverlib.ServerName(strings.TrimPrefix(baseURL, "https://"))
+
+	fedCli := gomatrixserverlib.NewFederationClient(
+		cfg.Global.SigningIdentities(),
+		gomatrixserverlib.WithSkipVerify(true),
+	)
+
 	for _, tc := range testCases {
 		ev, err := gomatrixserverlib.NewEventFromTrustedJSON([]byte(tc.eventJSON), false, tc.roomVer)
 		if err != nil {
diff --git a/federationapi/queue/queue_test.go b/federationapi/queue/queue_test.go
index bccfb342..55d1df4a 100644
--- a/federationapi/queue/queue_test.go
+++ b/federationapi/queue/queue_test.go
@@ -21,6 +21,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/test/testrig"
 	"go.uber.org/atomic"
 	"gotest.tools/v3/poll"
 
@@ -34,31 +37,29 @@ import (
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 )
 
 func mustCreateFederationDatabase(t *testing.T, dbType test.DBType, realDatabase bool) (storage.Database, *process.ProcessContext, func()) {
 	if realDatabase {
 		// Real Database/s
-		b, baseClose := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 		connStr, dbClose := test.PrepareDBConnectionString(t, dbType)
-		db, err := storage.NewDatabase(b, &config.DatabaseOptions{
+		db, err := storage.NewDatabase(processCtx.Context(), cm, &config.DatabaseOptions{
 			ConnectionString: config.DataSource(connStr),
-		}, b.Caches, b.Cfg.Global.IsLocalServerName)
+		}, caches, cfg.Global.IsLocalServerName)
 		if err != nil {
 			t.Fatalf("NewDatabase returned %s", err)
 		}
-		return db, b.ProcessContext, func() {
+		return db, processCtx, func() {
+			close()
 			dbClose()
-			baseClose()
 		}
 	} else {
 		// Fake Database
 		db := test.NewInMemoryFederationDatabase()
-		b := struct {
-			ProcessContext *process.ProcessContext
-		}{ProcessContext: process.NewProcessContext()}
-		return db, b.ProcessContext, func() {}
+		return db, process.NewProcessContext(), func() {}
 	}
 }
 
diff --git a/federationapi/routing/profile_test.go b/federationapi/routing/profile_test.go
index 3b9d576b..d5e9997f 100644
--- a/federationapi/routing/profile_test.go
+++ b/federationapi/routing/profile_test.go
@@ -27,7 +27,10 @@ import (
 	fedAPI "github.com/matrix-org/dendrite/federationapi"
 	fedInternal "github.com/matrix-org/dendrite/federationapi/internal"
 	"github.com/matrix-org/dendrite/federationapi/routing"
+	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
 	userAPI "github.com/matrix-org/dendrite/userapi/api"
@@ -46,23 +49,26 @@ func (u *fakeUserAPI) QueryProfile(ctx context.Context, req *userAPI.QueryProfil
 
 func TestHandleQueryProfile(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		routers := httputil.NewRouters()
 		defer close()
 
 		fedMux := mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicFederationPathPrefix).Subrouter().UseEncodedPath()
-		base.PublicFederationAPIMux = fedMux
-		base.Cfg.FederationAPI.Matrix.SigningIdentity.ServerName = testOrigin
-		base.Cfg.FederationAPI.Matrix.Metrics.Enabled = false
+		natsInstance := jetstream.NATSInstance{}
+		routers.Federation = fedMux
+		cfg.FederationAPI.Matrix.SigningIdentity.ServerName = testOrigin
+		cfg.FederationAPI.Matrix.Metrics.Enabled = false
 		fedClient := fakeFedClient{}
 		serverKeyAPI := &signing.YggdrasilKeys{}
 		keyRing := serverKeyAPI.KeyRing()
-		fedapi := fedAPI.NewInternalAPI(base, &fedClient, nil, nil, keyRing, true)
+		fedapi := fedAPI.NewInternalAPI(processCtx, cfg, cm, &natsInstance, &fedClient, nil, nil, keyRing, true)
 		userapi := fakeUserAPI{}
 		r, ok := fedapi.(*fedInternal.FederationInternalAPI)
 		if !ok {
 			panic("This is a programming error.")
 		}
-		routing.Setup(base, nil, r, keyRing, &fedClient, &userapi, &base.Cfg.MSCs, nil, nil)
+		routing.Setup(routers, cfg, nil, r, keyRing, &fedClient, &userapi, &cfg.MSCs, nil, nil, caching.DisableMetrics)
 
 		handler := fedMux.Get(routing.QueryProfileRouteName).GetHandler().ServeHTTP
 		_, sk, _ := ed25519.GenerateKey(nil)
diff --git a/federationapi/routing/query_test.go b/federationapi/routing/query_test.go
index d839a16b..6e702963 100644
--- a/federationapi/routing/query_test.go
+++ b/federationapi/routing/query_test.go
@@ -28,7 +28,10 @@ import (
 	fedclient "github.com/matrix-org/dendrite/federationapi/api"
 	fedInternal "github.com/matrix-org/dendrite/federationapi/internal"
 	"github.com/matrix-org/dendrite/federationapi/routing"
+	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/gomatrixserverlib"
@@ -46,23 +49,26 @@ func (f *fakeFedClient) LookupRoomAlias(ctx context.Context, origin, s gomatrixs
 
 func TestHandleQueryDirectory(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		routers := httputil.NewRouters()
 		defer close()
 
 		fedMux := mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicFederationPathPrefix).Subrouter().UseEncodedPath()
-		base.PublicFederationAPIMux = fedMux
-		base.Cfg.FederationAPI.Matrix.SigningIdentity.ServerName = testOrigin
-		base.Cfg.FederationAPI.Matrix.Metrics.Enabled = false
+		natsInstance := jetstream.NATSInstance{}
+		routers.Federation = fedMux
+		cfg.FederationAPI.Matrix.SigningIdentity.ServerName = testOrigin
+		cfg.FederationAPI.Matrix.Metrics.Enabled = false
 		fedClient := fakeFedClient{}
 		serverKeyAPI := &signing.YggdrasilKeys{}
 		keyRing := serverKeyAPI.KeyRing()
-		fedapi := fedAPI.NewInternalAPI(base, &fedClient, nil, nil, keyRing, true)
+		fedapi := fedAPI.NewInternalAPI(processCtx, cfg, cm, &natsInstance, &fedClient, nil, nil, keyRing, true)
 		userapi := fakeUserAPI{}
 		r, ok := fedapi.(*fedInternal.FederationInternalAPI)
 		if !ok {
 			panic("This is a programming error.")
 		}
-		routing.Setup(base, nil, r, keyRing, &fedClient, &userapi, &base.Cfg.MSCs, nil, nil)
+		routing.Setup(routers, cfg, nil, r, keyRing, &fedClient, &userapi, &cfg.MSCs, nil, nil, caching.DisableMetrics)
 
 		handler := fedMux.Get(routing.QueryDirectoryRouteName).GetHandler().ServeHTTP
 		_, sk, _ := ed25519.GenerateKey(nil)
diff --git a/federationapi/routing/send_test.go b/federationapi/routing/send_test.go
index eed4e7e6..28fa6d6d 100644
--- a/federationapi/routing/send_test.go
+++ b/federationapi/routing/send_test.go
@@ -25,7 +25,10 @@ import (
 	fedAPI "github.com/matrix-org/dendrite/federationapi"
 	fedInternal "github.com/matrix-org/dendrite/federationapi/internal"
 	"github.com/matrix-org/dendrite/federationapi/routing"
+	"github.com/matrix-org/dendrite/internal/caching"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/gomatrixserverlib"
@@ -44,21 +47,24 @@ type sendContent struct {
 
 func TestHandleSend(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		routers := httputil.NewRouters()
 		defer close()
 
 		fedMux := mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicFederationPathPrefix).Subrouter().UseEncodedPath()
-		base.PublicFederationAPIMux = fedMux
-		base.Cfg.FederationAPI.Matrix.SigningIdentity.ServerName = testOrigin
-		base.Cfg.FederationAPI.Matrix.Metrics.Enabled = false
-		fedapi := fedAPI.NewInternalAPI(base, nil, nil, nil, nil, true)
+		natsInstance := jetstream.NATSInstance{}
+		routers.Federation = fedMux
+		cfg.FederationAPI.Matrix.SigningIdentity.ServerName = testOrigin
+		cfg.FederationAPI.Matrix.Metrics.Enabled = false
+		fedapi := fedAPI.NewInternalAPI(processCtx, cfg, cm, &natsInstance, nil, nil, nil, nil, true)
 		serverKeyAPI := &signing.YggdrasilKeys{}
 		keyRing := serverKeyAPI.KeyRing()
 		r, ok := fedapi.(*fedInternal.FederationInternalAPI)
 		if !ok {
 			panic("This is a programming error.")
 		}
-		routing.Setup(base, nil, r, keyRing, nil, nil, &base.Cfg.MSCs, nil, nil)
+		routing.Setup(routers, cfg, nil, r, keyRing, nil, nil, &cfg.MSCs, nil, nil, caching.DisableMetrics)
 
 		handler := fedMux.Get(routing.SendRouteName).GetHandler().ServeHTTP
 		_, sk, _ := ed25519.GenerateKey(nil)
diff --git a/federationapi/storage/storage_test.go b/federationapi/storage/storage_test.go
index 1d2a13e8..74863c07 100644
--- a/federationapi/storage/storage_test.go
+++ b/federationapi/storage/storage_test.go
@@ -7,26 +7,28 @@ import (
 	"time"
 
 	"github.com/matrix-org/dendrite/federationapi/storage"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
 	"github.com/stretchr/testify/assert"
 )
 
 func mustCreateFederationDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) {
-	b, baseClose := testrig.CreateBaseDendrite(t, dbType)
+	caches := caching.NewRistrettoCache(8*1024*1024, time.Hour, false)
 	connStr, dbClose := test.PrepareDBConnectionString(t, dbType)
-	db, err := storage.NewDatabase(b, &config.DatabaseOptions{
+	ctx := context.Background()
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewDatabase(ctx, cm, &config.DatabaseOptions{
 		ConnectionString: config.DataSource(connStr),
-	}, b.Caches, func(server gomatrixserverlib.ServerName) bool { return server == "localhost" })
+	}, caches, func(server gomatrixserverlib.ServerName) bool { return server == "localhost" })
 	if err != nil {
 		t.Fatalf("NewDatabase returned %s", err)
 	}
 	return db, func() {
 		dbClose()
-		baseClose()
 	}
 }
 
diff --git a/internal/fulltext/bleve_test.go b/internal/fulltext/bleve_test.go
index d16397a4..bd8289d5 100644
--- a/internal/fulltext/bleve_test.go
+++ b/internal/fulltext/bleve_test.go
@@ -18,6 +18,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
 
@@ -25,7 +26,7 @@ import (
 	"github.com/matrix-org/dendrite/setup/config"
 )
 
-func mustOpenIndex(t *testing.T, tempDir string) *fulltext.Search {
+func mustOpenIndex(t *testing.T, tempDir string) (*fulltext.Search, *process.ProcessContext) {
 	t.Helper()
 	cfg := config.Fulltext{
 		Enabled:  true,
@@ -36,11 +37,12 @@ func mustOpenIndex(t *testing.T, tempDir string) *fulltext.Search {
 		cfg.IndexPath = config.Path(tempDir)
 		cfg.InMemory = false
 	}
-	fts, err := fulltext.New(cfg)
+	ctx := process.NewProcessContext()
+	fts, err := fulltext.New(ctx, cfg)
 	if err != nil {
 		t.Fatal("failed to open fulltext index:", err)
 	}
-	return fts
+	return fts, ctx
 }
 
 func mustAddTestData(t *testing.T, fts *fulltext.Search, firstStreamPos int64) (eventIDs, roomIDs []string) {
@@ -93,19 +95,17 @@ func mustAddTestData(t *testing.T, fts *fulltext.Search, firstStreamPos int64) (
 
 func TestOpen(t *testing.T) {
 	dataDir := t.TempDir()
-	fts := mustOpenIndex(t, dataDir)
-	if err := fts.Close(); err != nil {
-		t.Fatal("unable to close fulltext index", err)
-	}
+	_, ctx := mustOpenIndex(t, dataDir)
+	ctx.ShutdownDendrite()
 
 	// open existing index
-	fts = mustOpenIndex(t, dataDir)
-	defer fts.Close()
+	_, ctx = mustOpenIndex(t, dataDir)
+	ctx.ShutdownDendrite()
 }
 
 func TestIndex(t *testing.T) {
-	fts := mustOpenIndex(t, "")
-	defer fts.Close()
+	fts, ctx := mustOpenIndex(t, "")
+	defer ctx.ShutdownDendrite()
 
 	// add some data
 	var streamPos int64 = 1
@@ -128,8 +128,8 @@ func TestIndex(t *testing.T) {
 }
 
 func TestDelete(t *testing.T) {
-	fts := mustOpenIndex(t, "")
-	defer fts.Close()
+	fts, ctx := mustOpenIndex(t, "")
+	defer ctx.ShutdownDendrite()
 	eventIDs, roomIDs := mustAddTestData(t, fts, 0)
 	res1, err := fts.Search("lorem", roomIDs[:1], nil, 50, 0, false)
 	if err != nil {
@@ -224,7 +224,8 @@ func TestSearch(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f := mustOpenIndex(t, "")
+			f, ctx := mustOpenIndex(t, "")
+			defer ctx.ShutdownDendrite()
 			eventIDs, roomIDs := mustAddTestData(t, f, 0)
 			var searchRooms []string
 			for _, x := range tt.args.roomIndex {
diff --git a/internal/httputil/routing_test.go b/internal/httputil/routing_test.go
new file mode 100644
index 00000000..21e2bf48
--- /dev/null
+++ b/internal/httputil/routing_test.go
@@ -0,0 +1,38 @@
+package httputil
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"path/filepath"
+	"testing"
+)
+
+func TestRoutersError(t *testing.T) {
+	r := NewRouters()
+
+	// not found test
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, filepath.Join(PublicFederationPathPrefix, "doesnotexist"), nil)
+	r.Federation.ServeHTTP(rec, req)
+	if rec.Code != http.StatusNotFound {
+		t.Fatalf("unexpected status code: %d - %s", rec.Code, rec.Body.String())
+	}
+	if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
+		t.Fatalf("unexpected content-type: %s", ct)
+	}
+
+	// not allowed test
+	r.DendriteAdmin.
+		Handle("/test", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {})).
+		Methods(http.MethodPost)
+
+	rec = httptest.NewRecorder()
+	req = httptest.NewRequest(http.MethodGet, filepath.Join(DendriteAdminPathPrefix, "test"), nil)
+	r.DendriteAdmin.ServeHTTP(rec, req)
+	if rec.Code != http.StatusMethodNotAllowed {
+		t.Fatalf("unexpected status code: %d - %s", rec.Code, rec.Body.String())
+	}
+	if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
+		t.Fatalf("unexpected content-type: %s", ct)
+	}
+}
diff --git a/internal/sqlutil/connection_manager_test.go b/internal/sqlutil/connection_manager_test.go
new file mode 100644
index 00000000..a9ac8d57
--- /dev/null
+++ b/internal/sqlutil/connection_manager_test.go
@@ -0,0 +1,56 @@
+package sqlutil_test
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/test"
+)
+
+func TestConnectionManager(t *testing.T) {
+	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
+		conStr, close := test.PrepareDBConnectionString(t, dbType)
+		t.Cleanup(close)
+		cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+
+		dbProps := &config.DatabaseOptions{ConnectionString: config.DataSource(conStr)}
+		db, writer, err := cm.Connection(dbProps)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		switch dbType {
+		case test.DBTypeSQLite:
+			_, ok := writer.(*sqlutil.ExclusiveWriter)
+			if !ok {
+				t.Fatalf("expected exclusive writer")
+			}
+		case test.DBTypePostgres:
+			_, ok := writer.(*sqlutil.DummyWriter)
+			if !ok {
+				t.Fatalf("expected dummy writer")
+			}
+		}
+
+		// test global db pool
+		dbGlobal, writerGlobal, err := cm.Connection(&config.DatabaseOptions{})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !reflect.DeepEqual(db, dbGlobal) {
+			t.Fatalf("expected database connection to be reused")
+		}
+		if !reflect.DeepEqual(writer, writerGlobal) {
+			t.Fatalf("expected database writer to be reused")
+		}
+
+		// test invalid connection string configured
+		cm2 := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+		_, _, err = cm2.Connection(&config.DatabaseOptions{ConnectionString: "http://"})
+		if err == nil {
+			t.Fatal("expected an error but got none")
+		}
+	})
+}
diff --git a/internal/transactionrequest_test.go b/internal/transactionrequest_test.go
index 8597ae24..c152eb28 100644
--- a/internal/transactionrequest_test.go
+++ b/internal/transactionrequest_test.go
@@ -22,6 +22,12 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/gomatrixserverlib"
+	"github.com/nats-io/nats.go"
+	"github.com/stretchr/testify/assert"
+	"go.uber.org/atomic"
+	"gotest.tools/v3/poll"
+
 	"github.com/matrix-org/dendrite/federationapi/producers"
 	rsAPI "github.com/matrix-org/dendrite/roomserver/api"
 	"github.com/matrix-org/dendrite/setup/config"
@@ -30,11 +36,6 @@ import (
 	"github.com/matrix-org/dendrite/syncapi/types"
 	"github.com/matrix-org/dendrite/test"
 	keyAPI "github.com/matrix-org/dendrite/userapi/api"
-	"github.com/matrix-org/gomatrixserverlib"
-	"github.com/nats-io/nats.go"
-	"github.com/stretchr/testify/assert"
-	"go.uber.org/atomic"
-	"gotest.tools/v3/poll"
 )
 
 const (
@@ -427,7 +428,7 @@ func TestProcessTransactionRequestEDUReceipt(t *testing.T) {
 		roomID: map[string]interface{}{
 			"m.read": map[string]interface{}{
 				"@john:kaer.morhen": map[string]interface{}{
-					"data": map[string]interface{}{
+					"data": map[string]int64{
 						"ts": 1533358089009,
 					},
 					"event_ids": []string{
@@ -446,7 +447,7 @@ func TestProcessTransactionRequestEDUReceipt(t *testing.T) {
 		roomID: map[string]interface{}{
 			"m.read": map[string]interface{}{
 				"johnkaer.morhen": map[string]interface{}{
-					"data": map[string]interface{}{
+					"data": map[string]int64{
 						"ts": 1533358089009,
 					},
 					"event_ids": []string{
@@ -463,7 +464,7 @@ func TestProcessTransactionRequestEDUReceipt(t *testing.T) {
 		roomID: map[string]interface{}{
 			"m.read": map[string]interface{}{
 				"@john:bad.domain": map[string]interface{}{
-					"data": map[string]interface{}{
+					"data": map[string]int64{
 						"ts": 1533358089009,
 					},
 					"event_ids": []string{
diff --git a/mediaapi/routing/upload_test.go b/mediaapi/routing/upload_test.go
index 420d0eba..d088950c 100644
--- a/mediaapi/routing/upload_test.go
+++ b/mediaapi/routing/upload_test.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/fileutils"
 	"github.com/matrix-org/dendrite/mediaapi/storage"
 	"github.com/matrix-org/dendrite/mediaapi/types"
@@ -49,8 +50,8 @@ func Test_uploadRequest_doUpload(t *testing.T) {
 	// create testdata folder and remove when done
 	_ = os.Mkdir(testdataPath, os.ModePerm)
 	defer fileutils.RemoveDir(types.Path(testdataPath), nil)
-
-	db, err := storage.NewMediaAPIDatasource(nil, &config.DatabaseOptions{
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewMediaAPIDatasource(cm, &config.DatabaseOptions{
 		ConnectionString:       "file::memory:?cache=shared",
 		MaxOpenConnections:     100,
 		MaxIdleConnections:     2,
diff --git a/mediaapi/storage/storage_test.go b/mediaapi/storage/storage_test.go
index 81f0a5d2..8cd29a54 100644
--- a/mediaapi/storage/storage_test.go
+++ b/mediaapi/storage/storage_test.go
@@ -5,6 +5,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/mediaapi/storage"
 	"github.com/matrix-org/dendrite/mediaapi/types"
 	"github.com/matrix-org/dendrite/setup/config"
@@ -13,7 +14,8 @@ import (
 
 func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) {
 	connStr, close := test.PrepareDBConnectionString(t, dbType)
-	db, err := storage.NewMediaAPIDatasource(nil, &config.DatabaseOptions{
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewMediaAPIDatasource(cm, &config.DatabaseOptions{
 		ConnectionString: config.DataSource(connStr),
 	})
 	if err != nil {
diff --git a/relayapi/relayapi_test.go b/relayapi/relayapi_test.go
index f1b3262a..8973c8cc 100644
--- a/relayapi/relayapi_test.go
+++ b/relayapi/relayapi_test.go
@@ -21,9 +21,13 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"testing"
+	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/relayapi"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
@@ -33,37 +37,38 @@ import (
 
 func TestCreateNewRelayInternalAPI(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 		defer close()
-
-		relayAPI := relayapi.NewRelayInternalAPI(base, nil, nil, nil, nil, true)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		relayAPI := relayapi.NewRelayInternalAPI(cfg, cm, nil, nil, nil, nil, true, caches)
 		assert.NotNil(t, relayAPI)
 	})
 }
 
 func TestCreateRelayInternalInvalidDatabasePanics(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
 		if dbType == test.DBTypeSQLite {
-			base.Cfg.RelayAPI.Database.ConnectionString = "file:"
+			cfg.RelayAPI.Database.ConnectionString = "file:"
 		} else {
-			base.Cfg.RelayAPI.Database.ConnectionString = "test"
+			cfg.RelayAPI.Database.ConnectionString = "test"
 		}
 		defer close()
-
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
 		assert.Panics(t, func() {
-			relayapi.NewRelayInternalAPI(base, nil, nil, nil, nil, true)
+			relayapi.NewRelayInternalAPI(cfg, cm, nil, nil, nil, nil, true, nil)
 		})
 	})
 }
 
 func TestCreateInvalidRelayPublicRoutesPanics(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, _, close := testrig.CreateConfig(t, dbType)
 		defer close()
-
+		routers := httputil.NewRouters()
 		assert.Panics(t, func() {
-			relayapi.AddPublicRoutes(base, nil, nil)
+			relayapi.AddPublicRoutes(routers, cfg, nil, nil)
 		})
 	})
 }
@@ -105,15 +110,19 @@ func createSendRelayTxnHTTPRequest(serverName gomatrixserverlib.ServerName, txnI
 
 func TestCreateRelayPublicRoutes(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
 		defer close()
+		routers := httputil.NewRouters()
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 
-		relayAPI := relayapi.NewRelayInternalAPI(base, nil, nil, nil, nil, true)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+
+		relayAPI := relayapi.NewRelayInternalAPI(cfg, cm, nil, nil, nil, nil, true, caches)
 		assert.NotNil(t, relayAPI)
 
 		serverKeyAPI := &signing.YggdrasilKeys{}
 		keyRing := serverKeyAPI.KeyRing()
-		relayapi.AddPublicRoutes(base, keyRing, relayAPI)
+		relayapi.AddPublicRoutes(routers, cfg, keyRing, relayAPI)
 
 		testCases := []struct {
 			name     string
@@ -122,29 +131,29 @@ func TestCreateRelayPublicRoutes(t *testing.T) {
 		}{
 			{
 				name:     "relay_txn invalid user id",
-				req:      createGetRelayTxnHTTPRequest(base.Cfg.Global.ServerName, "user:local"),
+				req:      createGetRelayTxnHTTPRequest(cfg.Global.ServerName, "user:local"),
 				wantCode: 400,
 			},
 			{
 				name:     "relay_txn valid user id",
-				req:      createGetRelayTxnHTTPRequest(base.Cfg.Global.ServerName, "@user:local"),
+				req:      createGetRelayTxnHTTPRequest(cfg.Global.ServerName, "@user:local"),
 				wantCode: 200,
 			},
 			{
 				name:     "send_relay invalid user id",
-				req:      createSendRelayTxnHTTPRequest(base.Cfg.Global.ServerName, "123", "user:local"),
+				req:      createSendRelayTxnHTTPRequest(cfg.Global.ServerName, "123", "user:local"),
 				wantCode: 400,
 			},
 			{
 				name:     "send_relay valid user id",
-				req:      createSendRelayTxnHTTPRequest(base.Cfg.Global.ServerName, "123", "@user:local"),
+				req:      createSendRelayTxnHTTPRequest(cfg.Global.ServerName, "123", "@user:local"),
 				wantCode: 200,
 			},
 		}
 
 		for _, tc := range testCases {
 			w := httptest.NewRecorder()
-			base.PublicFederationAPIMux.ServeHTTP(w, tc.req)
+			routers.Federation.ServeHTTP(w, tc.req)
 			if w.Code != tc.wantCode {
 				t.Fatalf("%s: got HTTP %d want %d", tc.name, w.Code, tc.wantCode)
 			}
@@ -154,15 +163,19 @@ func TestCreateRelayPublicRoutes(t *testing.T) {
 
 func TestDisableRelayPublicRoutes(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
 		defer close()
+		routers := httputil.NewRouters()
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
 
-		relayAPI := relayapi.NewRelayInternalAPI(base, nil, nil, nil, nil, false)
+		relayAPI := relayapi.NewRelayInternalAPI(cfg, cm, nil, nil, nil, nil, false, caches)
 		assert.NotNil(t, relayAPI)
 
 		serverKeyAPI := &signing.YggdrasilKeys{}
 		keyRing := serverKeyAPI.KeyRing()
-		relayapi.AddPublicRoutes(base, keyRing, relayAPI)
+		relayapi.AddPublicRoutes(routers, cfg, keyRing, relayAPI)
 
 		testCases := []struct {
 			name     string
@@ -171,19 +184,19 @@ func TestDisableRelayPublicRoutes(t *testing.T) {
 		}{
 			{
 				name:     "relay_txn valid user id",
-				req:      createGetRelayTxnHTTPRequest(base.Cfg.Global.ServerName, "@user:local"),
+				req:      createGetRelayTxnHTTPRequest(cfg.Global.ServerName, "@user:local"),
 				wantCode: 404,
 			},
 			{
 				name:     "send_relay valid user id",
-				req:      createSendRelayTxnHTTPRequest(base.Cfg.Global.ServerName, "123", "@user:local"),
+				req:      createSendRelayTxnHTTPRequest(cfg.Global.ServerName, "123", "@user:local"),
 				wantCode: 404,
 			},
 		}
 
 		for _, tc := range testCases {
 			w := httptest.NewRecorder()
-			base.PublicFederationAPIMux.ServeHTTP(w, tc.req)
+			routers.Federation.ServeHTTP(w, tc.req)
 			if w.Code != tc.wantCode {
 				t.Fatalf("%s: got HTTP %d want %d", tc.name, w.Code, tc.wantCode)
 			}
diff --git a/roomserver/internal/helpers/helpers_test.go b/roomserver/internal/helpers/helpers_test.go
index c056e704..dd74b844 100644
--- a/roomserver/internal/helpers/helpers_test.go
+++ b/roomserver/internal/helpers/helpers_test.go
@@ -3,26 +3,28 @@ package helpers
 import (
 	"context"
 	"testing"
+	"time"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/matrix-org/dendrite/roomserver/types"
 
-	"github.com/matrix-org/dendrite/setup/base"
-
-	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
-
 	"github.com/matrix-org/dendrite/roomserver/storage"
+	"github.com/matrix-org/dendrite/test"
 )
 
-func mustCreateDatabase(t *testing.T, dbType test.DBType) (*base.BaseDendrite, storage.Database, func()) {
-	base, close := testrig.CreateBaseDendrite(t, dbType)
-	db, err := storage.Open(base, &base.Cfg.RoomServer.Database, base.Caches)
+func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) {
+	conStr, close := test.PrepareDBConnectionString(t, dbType)
+	caches := caching.NewRistrettoCache(8*1024*1024, time.Hour, caching.DisableMetrics)
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.Open(context.Background(), cm, &config.DatabaseOptions{ConnectionString: config.DataSource(conStr)}, caches)
 	if err != nil {
 		t.Fatalf("failed to create Database: %v", err)
 	}
-	return base, db, close
+	return db, close
 }
 
 func TestIsInvitePendingWithoutNID(t *testing.T) {
@@ -32,7 +34,7 @@ func TestIsInvitePendingWithoutNID(t *testing.T) {
 	room := test.NewRoom(t, alice, test.RoomPreset(test.PresetPublicChat))
 	_ = bob
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		_, db, close := mustCreateDatabase(t, dbType)
+		db, close := mustCreateDatabase(t, dbType)
 		defer close()
 
 		// store all events
diff --git a/roomserver/internal/input/input_test.go b/roomserver/internal/input/input_test.go
index 4708560a..51c50c37 100644
--- a/roomserver/internal/input/input_test.go
+++ b/roomserver/internal/input/input_test.go
@@ -2,82 +2,69 @@ package input_test
 
 import (
 	"context"
-	"os"
 	"testing"
 	"time"
 
 	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/roomserver/api"
 	"github.com/matrix-org/dendrite/roomserver/internal/input"
-	"github.com/matrix-org/dendrite/roomserver/storage"
-	"github.com/matrix-org/dendrite/setup/base"
-	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/jetstream"
+	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/gomatrixserverlib"
-	"github.com/nats-io/nats.go"
 )
 
-var js nats.JetStreamContext
-var jc *nats.Conn
+func TestSingleTransactionOnInput(t *testing.T) {
+	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		defer close()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
 
-func TestMain(m *testing.M) {
-	var b *base.BaseDendrite
-	b, js, jc = testrig.Base(nil)
-	code := m.Run()
-	b.ShutdownDendrite()
-	b.WaitForComponentsToFinish()
-	os.Exit(code)
-}
+		natsInstance := &jetstream.NATSInstance{}
+		js, jc := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+		caches := caching.NewRistrettoCache(8*1024*1024, time.Hour, caching.DisableMetrics)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
+		rsAPI.SetFederationAPI(nil, nil)
 
-func TestSingleTransactionOnInput(t *testing.T) {
-	deadline, _ := t.Deadline()
-	if max := time.Now().Add(time.Second * 3); deadline.After(max) {
-		deadline = max
-	}
-	ctx, cancel := context.WithDeadline(context.Background(), deadline)
-	defer cancel()
+		deadline, _ := t.Deadline()
+		if max := time.Now().Add(time.Second * 3); deadline.Before(max) {
+			deadline = max
+		}
+		ctx, cancel := context.WithDeadline(processCtx.Context(), deadline)
+		defer cancel()
+
+		event, err := gomatrixserverlib.NewEventFromTrustedJSON(
+			[]byte(`{"auth_events":[],"content":{"creator":"@neilalexander:dendrite.matrix.org","room_version":"6"},"depth":1,"hashes":{"sha256":"jqOqdNEH5r0NiN3xJtj0u5XUVmRqq9YvGbki1wxxuuM"},"origin":"dendrite.matrix.org","origin_server_ts":1644595362726,"prev_events":[],"prev_state":[],"room_id":"!jSZZRknA6GkTBXNP:dendrite.matrix.org","sender":"@neilalexander:dendrite.matrix.org","signatures":{"dendrite.matrix.org":{"ed25519:6jB2aB":"bsQXO1wketf1OSe9xlndDIWe71W9KIundc6rBw4KEZdGPW7x4Tv4zDWWvbxDsG64sS2IPWfIm+J0OOozbrWIDw"}},"state_key":"","type":"m.room.create"}`),
+			false, gomatrixserverlib.RoomVersionV6,
+		)
+		if err != nil {
+			t.Fatal(err)
+		}
+		in := api.InputRoomEvent{
+			Kind:  api.KindOutlier, // don't panic if we generate an output event
+			Event: event.Headered(gomatrixserverlib.RoomVersionV6),
+		}
 
-	event, err := gomatrixserverlib.NewEventFromTrustedJSON(
-		[]byte(`{"auth_events":[],"content":{"creator":"@neilalexander:dendrite.matrix.org","room_version":"6"},"depth":1,"hashes":{"sha256":"jqOqdNEH5r0NiN3xJtj0u5XUVmRqq9YvGbki1wxxuuM"},"origin":"dendrite.matrix.org","origin_server_ts":1644595362726,"prev_events":[],"prev_state":[],"room_id":"!jSZZRknA6GkTBXNP:dendrite.matrix.org","sender":"@neilalexander:dendrite.matrix.org","signatures":{"dendrite.matrix.org":{"ed25519:6jB2aB":"bsQXO1wketf1OSe9xlndDIWe71W9KIundc6rBw4KEZdGPW7x4Tv4zDWWvbxDsG64sS2IPWfIm+J0OOozbrWIDw"}},"state_key":"","type":"m.room.create"}`),
-		false, gomatrixserverlib.RoomVersionV6,
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
-	in := api.InputRoomEvent{
-		Kind:  api.KindOutlier, // don't panic if we generate an output event
-		Event: event.Headered(gomatrixserverlib.RoomVersionV6),
-	}
-	db, err := storage.Open(
-		nil,
-		&config.DatabaseOptions{
-			ConnectionString:   "",
-			MaxOpenConnections: 1,
-			MaxIdleConnections: 1,
-		},
-		caching.NewRistrettoCache(8*1024*1024, time.Hour, false),
-	)
-	if err != nil {
-		t.Logf("PostgreSQL not available (%s), skipping", err)
-		t.SkipNow()
-	}
-	inputter := &input.Inputer{
-		DB:         db,
-		JetStream:  js,
-		NATSClient: jc,
-	}
-	res := &api.InputRoomEventsResponse{}
-	inputter.InputRoomEvents(
-		ctx,
-		&api.InputRoomEventsRequest{
-			InputRoomEvents: []api.InputRoomEvent{in},
-			Asynchronous:    false,
-		},
-		res,
-	)
-	// If we fail here then it's because we've hit the test deadline,
-	// so we probably deadlocked
-	if err := res.Err(); err != nil {
-		t.Fatal(err)
-	}
+		inputter := &input.Inputer{
+			JetStream:  js,
+			NATSClient: jc,
+			Cfg:        &cfg.RoomServer,
+		}
+		res := &api.InputRoomEventsResponse{}
+		inputter.InputRoomEvents(
+			ctx,
+			&api.InputRoomEventsRequest{
+				InputRoomEvents: []api.InputRoomEvent{in},
+				Asynchronous:    false,
+			},
+			res,
+		)
+		// If we fail here then it's because we've hit the test deadline,
+		// so we probably deadlocked
+		if err := res.Err(); err != nil {
+			t.Fatal(err)
+		}
+	})
 }
diff --git a/roomserver/roomserver_test.go b/roomserver/roomserver_test.go
index a3ca5909..729da15b 100644
--- a/roomserver/roomserver_test.go
+++ b/roomserver/roomserver_test.go
@@ -7,11 +7,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/matrix-org/dendrite/roomserver/state"
 	"github.com/matrix-org/dendrite/roomserver/types"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/userapi"
 
 	userAPI "github.com/matrix-org/dendrite/userapi/api"
@@ -29,21 +31,14 @@ import (
 	"github.com/matrix-org/dendrite/test/testrig"
 )
 
-func mustCreateDatabase(t *testing.T, dbType test.DBType) (*base.BaseDendrite, storage.Database, func()) {
-	t.Helper()
-	base, close := testrig.CreateBaseDendrite(t, dbType)
-	db, err := storage.Open(base, &base.Cfg.RoomServer.Database, base.Caches)
-	if err != nil {
-		t.Fatalf("failed to create Database: %v", err)
-	}
-	return base, db, close
-}
-
 func TestUsers(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
 		defer close()
-		rsAPI := roomserver.NewInternalAPI(base)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		natsInstance := jetstream.NATSInstance{}
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 		// SetFederationAPI starts the room event input consumer
 		rsAPI.SetFederationAPI(nil, nil)
 
@@ -52,7 +47,7 @@ func TestUsers(t *testing.T) {
 		})
 
 		t.Run("kick users", func(t *testing.T) {
-			usrAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+			usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 			rsAPI.SetUserAPI(usrAPI)
 			testKickUsers(t, rsAPI, usrAPI)
 		})
@@ -178,10 +173,13 @@ func Test_QueryLeftUsers(t *testing.T) {
 
 	ctx := context.Background()
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, _, close := mustCreateDatabase(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
 		defer close()
 
-		rsAPI := roomserver.NewInternalAPI(base)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		natsInstance := jetstream.NATSInstance{}
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 		// SetFederationAPI starts the room event input consumer
 		rsAPI.SetFederationAPI(nil, nil)
 		// Create the room
@@ -225,29 +223,35 @@ func TestPurgeRoom(t *testing.T) {
 	ctx := context.Background()
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, db, close := mustCreateDatabase(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		natsInstance := jetstream.NATSInstance{}
 		defer close()
+		routers := httputil.NewRouters()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		db, err := storage.Open(processCtx.Context(), cm, &cfg.RoomServer.Database, caches)
+		if err != nil {
+			t.Fatal(err)
+		}
+		jsCtx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+		defer jetstream.DeleteAllStreams(jsCtx, &cfg.Global.JetStream)
 
-		jsCtx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-		defer jetstream.DeleteAllStreams(jsCtx, &base.Cfg.Global.JetStream)
-
-		fedClient := base.CreateFederationClient()
-		rsAPI := roomserver.NewInternalAPI(base)
-		userAPI := userapi.NewInternalAPI(base, rsAPI, nil)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+		userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
 
 		// this starts the JetStream consumers
-		syncapi.AddPublicRoutes(base, userAPI, rsAPI)
-		federationapi.NewInternalAPI(base, fedClient, rsAPI, base.Caches, nil, true)
+		syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, userAPI, rsAPI, caches, caching.DisableMetrics)
+		federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, nil, rsAPI, caches, nil, true)
 		rsAPI.SetFederationAPI(nil, nil)
 
 		// Create the room
-		if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
+		if err = api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
 			t.Fatalf("failed to send events: %v", err)
 		}
 
 		// some dummy entries to validate after purging
 		publishResp := &api.PerformPublishResponse{}
-		if err := rsAPI.PerformPublish(ctx, &api.PerformPublishRequest{RoomID: room.ID, Visibility: "public"}, publishResp); err != nil {
+		if err = rsAPI.PerformPublish(ctx, &api.PerformPublishRequest{RoomID: room.ID, Visibility: "public"}, publishResp); err != nil {
 			t.Fatal(err)
 		}
 		if publishResp.Error != nil {
@@ -335,7 +339,7 @@ func TestPurgeRoom(t *testing.T) {
 				t.Fatalf("test timed out after %s", timeout)
 			}
 			sum = 0
-			consumerCh := jsCtx.Consumers(base.Cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent))
+			consumerCh := jsCtx.Consumers(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent))
 			for x := range consumerCh {
 				sum += x.NumAckPending
 			}
@@ -503,8 +507,14 @@ func TestRedaction(t *testing.T) {
 
 	ctx := context.Background()
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		_, db, close := mustCreateDatabase(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
 		defer close()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+		db, err := storage.Open(processCtx.Context(), cm, &cfg.RoomServer.Database, caches)
+		if err != nil {
+			t.Fatal(err)
+		}
 
 		for _, tc := range testCases {
 			t.Run(tc.name, func(t *testing.T) {
diff --git a/roomserver/storage/shared/storage_test.go b/roomserver/storage/shared/storage_test.go
index 684e80b8..941e8480 100644
--- a/roomserver/storage/shared/storage_test.go
+++ b/roomserver/storage/shared/storage_test.go
@@ -15,14 +15,12 @@ import (
 	"github.com/matrix-org/dendrite/roomserver/storage/tables"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 )
 
 func mustCreateRoomserverDatabase(t *testing.T, dbType test.DBType) (*shared.Database, func()) {
 	t.Helper()
 
 	connStr, clearDB := test.PrepareDBConnectionString(t, dbType)
-	base, _, _ := testrig.Base(nil)
 	dbOpts := &config.DatabaseOptions{ConnectionString: config.DataSource(connStr)}
 
 	db, err := sqlutil.Open(dbOpts, sqlutil.NewExclusiveWriter())
@@ -61,8 +59,6 @@ func mustCreateRoomserverDatabase(t *testing.T, dbType test.DBType) (*shared.Dat
 			Writer:          sqlutil.NewExclusiveWriter(),
 			Cache:           cache,
 		}, func() {
-			err := base.Close()
-			assert.NoError(t, err)
 			clearDB()
 			err = db.Close()
 			assert.NoError(t, err)
diff --git a/setup/base/base_test.go b/setup/base/base_test.go
index 658dc5b0..bba967b9 100644
--- a/setup/base/base_test.go
+++ b/setup/base/base_test.go
@@ -13,8 +13,10 @@ import (
 	"time"
 
 	"github.com/matrix-org/dendrite/internal"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	basepkg "github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
-	"github.com/matrix-org/dendrite/test/testrig"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -30,8 +32,10 @@ func TestLandingPage_Tcp(t *testing.T) {
 	})
 	assert.NoError(t, err)
 
-	b, _, _ := testrig.Base(nil)
-	defer b.Close()
+	processCtx := process.NewProcessContext()
+	routers := httputil.NewRouters()
+	cfg := config.Dendrite{}
+	cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
 
 	// hack: create a server and close it immediately, just to get a random port assigned
 	s := httptest.NewServer(nil)
@@ -40,7 +44,7 @@ func TestLandingPage_Tcp(t *testing.T) {
 	// start base with the listener and wait for it to be started
 	address, err := config.HTTPAddress(s.URL)
 	assert.NoError(t, err)
-	go b.SetupAndServeHTTP(address, nil, nil)
+	go basepkg.SetupAndServeHTTP(processCtx, &cfg, routers, address, nil, nil)
 	time.Sleep(time.Millisecond * 10)
 
 	// When hitting /, we should be redirected to /_matrix/static, which should contain the landing page
@@ -70,15 +74,17 @@ func TestLandingPage_UnixSocket(t *testing.T) {
 	})
 	assert.NoError(t, err)
 
-	b, _, _ := testrig.Base(nil)
-	defer b.Close()
+	processCtx := process.NewProcessContext()
+	routers := httputil.NewRouters()
+	cfg := config.Dendrite{}
+	cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
 
 	tempDir := t.TempDir()
 	socket := path.Join(tempDir, "socket")
 	// start base with the listener and wait for it to be started
-	address := config.UnixSocketAddress(socket, 0755)
+	address, err := config.UnixSocketAddress(socket, "755")
 	assert.NoError(t, err)
-	go b.SetupAndServeHTTP(address, nil, nil)
+	go basepkg.SetupAndServeHTTP(processCtx, &cfg, routers, address, nil, nil)
 	time.Sleep(time.Millisecond * 100)
 
 	client := &http.Client{
diff --git a/setup/config/config_address_test.go b/setup/config/config_address_test.go
index 1be484fd..38c96ab7 100644
--- a/setup/config/config_address_test.go
+++ b/setup/config/config_address_test.go
@@ -20,6 +20,24 @@ func TestHttpAddress_ParseBad(t *testing.T) {
 }
 
 func TestUnixSocketAddress_Network(t *testing.T) {
-	address := UnixSocketAddress("/tmp", fs.FileMode(0755))
+	address, err := UnixSocketAddress("/tmp", "0755")
+	assert.NoError(t, err)
 	assert.Equal(t, "unix", address.Network())
 }
+
+func TestUnixSocketAddress_Permission_LeadingZero_Ok(t *testing.T) {
+	address, err := UnixSocketAddress("/tmp", "0755")
+	assert.NoError(t, err)
+	assert.Equal(t, fs.FileMode(0755), address.UnixSocketPermission)
+}
+
+func TestUnixSocketAddress_Permission_NoLeadingZero_Ok(t *testing.T) {
+	address, err := UnixSocketAddress("/tmp", "755")
+	assert.NoError(t, err)
+	assert.Equal(t, fs.FileMode(0755), address.UnixSocketPermission)
+}
+
+func TestUnixSocketAddress_Permission_NonOctal_Bad(t *testing.T) {
+	_, err := UnixSocketAddress("/tmp", "855")
+	assert.Error(t, err)
+}
diff --git a/setup/mscs/msc2836/msc2836_test.go b/setup/mscs/msc2836/msc2836_test.go
index f12fbbfc..bfcabef6 100644
--- a/setup/mscs/msc2836/msc2836_test.go
+++ b/setup/mscs/msc2836/msc2836_test.go
@@ -15,12 +15,13 @@ import (
 	"time"
 
 	"github.com/gorilla/mux"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/gomatrixserverlib"
 
 	"github.com/matrix-org/dendrite/internal/hooks"
 	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	roomserver "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/mscs/msc2836"
 	userapi "github.com/matrix-org/dendrite/userapi/api"
@@ -554,20 +555,18 @@ func injectEvents(t *testing.T, userAPI userapi.UserInternalAPI, rsAPI roomserve
 	cfg.Global.ServerName = "localhost"
 	cfg.MSCs.Database.ConnectionString = "file:msc2836_test.db"
 	cfg.MSCs.MSCs = []string{"msc2836"}
-	base := &base.BaseDendrite{
-		Cfg:                    cfg,
-		PublicClientAPIMux:     mux.NewRouter().PathPrefix(httputil.PublicClientPathPrefix).Subrouter(),
-		PublicFederationAPIMux: mux.NewRouter().PathPrefix(httputil.PublicFederationPathPrefix).Subrouter(),
-	}
 
-	err := msc2836.Enable(base, rsAPI, nil, userAPI, nil)
+	processCtx := process.NewProcessContext()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	routers := httputil.NewRouters()
+	err := msc2836.Enable(cfg, cm, routers, rsAPI, nil, userAPI, nil)
 	if err != nil {
 		t.Fatalf("failed to enable MSC2836: %s", err)
 	}
 	for _, ev := range events {
 		hooks.Run(hooks.KindNewEventPersisted, ev)
 	}
-	return base.PublicClientAPIMux
+	return routers.Client
 }
 
 type fledglingEvent struct {
diff --git a/syncapi/storage/storage_test.go b/syncapi/storage/storage_test.go
index 05d498bc..e81a341f 100644
--- a/syncapi/storage/storage_test.go
+++ b/syncapi/storage/storage_test.go
@@ -9,27 +9,27 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/syncapi/storage"
 	"github.com/matrix-org/dendrite/syncapi/types"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/stretchr/testify/assert"
 )
 
 var ctx = context.Background()
 
-func MustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func(), func()) {
+func MustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) {
 	connStr, close := test.PrepareDBConnectionString(t, dbType)
-	base, closeBase := testrig.CreateBaseDendrite(t, dbType)
-	db, err := storage.NewSyncServerDatasource(base, &config.DatabaseOptions{
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewSyncServerDatasource(context.Background(), cm, &config.DatabaseOptions{
 		ConnectionString: config.DataSource(connStr),
 	})
 	if err != nil {
 		t.Fatalf("NewSyncServerDatasource returned %s", err)
 	}
-	return db, close, closeBase
+	return db, close
 }
 
 func MustWriteEvents(t *testing.T, db storage.Database, events []*gomatrixserverlib.HeaderedEvent) (positions []types.StreamPosition) {
@@ -55,9 +55,8 @@ func TestWriteEvents(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
 		alice := test.NewUser(t)
 		r := test.NewRoom(t, alice)
-		db, close, closeBase := MustCreateDatabase(t, dbType)
+		db, close := MustCreateDatabase(t, dbType)
 		defer close()
-		defer closeBase()
 		MustWriteEvents(t, db, r.Events())
 	})
 }
@@ -76,9 +75,8 @@ func WithSnapshot(t *testing.T, db storage.Database, f func(snapshot storage.Dat
 // These tests assert basic functionality of RecentEvents for PDUs
 func TestRecentEventsPDU(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		db, close, closeBase := MustCreateDatabase(t, dbType)
+		db, close := MustCreateDatabase(t, dbType)
 		defer close()
-		defer closeBase()
 		alice := test.NewUser(t)
 		// dummy room to make sure SQL queries are filtering on room ID
 		MustWriteEvents(t, db, test.NewRoom(t, alice).Events())
@@ -191,9 +189,8 @@ func TestRecentEventsPDU(t *testing.T) {
 // The purpose of this test is to ensure that backfill does indeed go backwards, using a topology token
 func TestGetEventsInRangeWithTopologyToken(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		db, close, closeBase := MustCreateDatabase(t, dbType)
+		db, close := MustCreateDatabase(t, dbType)
 		defer close()
-		defer closeBase()
 		alice := test.NewUser(t)
 		r := test.NewRoom(t, alice)
 		for i := 0; i < 10; i++ {
@@ -276,9 +273,8 @@ func TestStreamToTopologicalPosition(t *testing.T) {
 	}
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		db, close, closeBase := MustCreateDatabase(t, dbType)
+		db, close := MustCreateDatabase(t, dbType)
 		defer close()
-		defer closeBase()
 
 		txn, err := db.NewDatabaseTransaction(ctx)
 		if err != nil {
@@ -514,9 +510,8 @@ func TestSendToDeviceBehaviour(t *testing.T) {
 	bob := test.NewUser(t)
 	deviceID := "one"
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		db, close, closeBase := MustCreateDatabase(t, dbType)
+		db, close := MustCreateDatabase(t, dbType)
 		defer close()
-		defer closeBase()
 		// At this point there should be no messages. We haven't sent anything
 		// yet.
 
@@ -899,9 +894,8 @@ func TestRoomSummary(t *testing.T) {
 	}
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		db, close, closeBase := MustCreateDatabase(t, dbType)
+		db, close := MustCreateDatabase(t, dbType)
 		defer close()
-		defer closeBase()
 
 		for _, tc := range testCases {
 			t.Run(tc.name, func(t *testing.T) {
@@ -939,11 +933,8 @@ func TestRecentEvents(t *testing.T) {
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
 		filter := gomatrixserverlib.DefaultRoomEventFilter()
-		db, close, closeBase := MustCreateDatabase(t, dbType)
-		t.Cleanup(func() {
-			close()
-			closeBase()
-		})
+		db, close := MustCreateDatabase(t, dbType)
+		t.Cleanup(close)
 
 		MustWriteEvents(t, db, room1.Events())
 		MustWriteEvents(t, db, room2.Events())
diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go
index 1cb82ce1..584782af 100644
--- a/syncapi/syncapi_test.go
+++ b/syncapi/syncapi_test.go
@@ -10,6 +10,10 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/caching"
+	"github.com/matrix-org/dendrite/internal/httputil"
+	"github.com/matrix-org/dendrite/internal/sqlutil"
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/nats-io/nats.go"
 	"github.com/tidwall/gjson"
@@ -21,7 +25,6 @@ import (
 	"github.com/matrix-org/dendrite/roomserver"
 	"github.com/matrix-org/dendrite/roomserver/api"
 	rsapi "github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 	"github.com/matrix-org/dendrite/syncapi/types"
 	"github.com/matrix-org/dendrite/test"
@@ -113,13 +116,17 @@ func testSyncAccessTokens(t *testing.T, dbType test.DBType) {
 		AccountType: userapi.AccountTypeUser,
 	}
 
-	base, close := testrig.CreateBaseDendrite(t, dbType)
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	routers := httputil.NewRouters()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+	natsInstance := jetstream.NATSInstance{}
 	defer close()
 
-	jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-	defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
-	msgs := toNATSMsgs(t, base, room.Events()...)
-	AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}})
+	jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+	defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
+	msgs := toNATSMsgs(t, cfg, room.Events()...)
+	AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, caches, caching.DisableMetrics)
 	testrig.MustPublishMsgs(t, jsctx, msgs...)
 
 	testCases := []struct {
@@ -154,7 +161,7 @@ func testSyncAccessTokens(t *testing.T, dbType test.DBType) {
 		},
 	}
 
-	syncUntil(t, base, alice.AccessToken, false, func(syncBody string) bool {
+	syncUntil(t, routers, alice.AccessToken, false, func(syncBody string) bool {
 		// wait for the last sent eventID to come down sync
 		path := fmt.Sprintf(`rooms.join.%s.timeline.events.#(event_id=="%s")`, room.ID, room.Events()[len(room.Events())-1].EventID())
 		return gjson.Get(syncBody, path).Exists()
@@ -162,7 +169,7 @@ func testSyncAccessTokens(t *testing.T, dbType test.DBType) {
 
 	for _, tc := range testCases {
 		w := httptest.NewRecorder()
-		base.PublicClientAPIMux.ServeHTTP(w, tc.req)
+		routers.Client.ServeHTTP(w, tc.req)
 		if w.Code != tc.wantCode {
 			t.Fatalf("%s: got HTTP %d want %d", tc.name, w.Code, tc.wantCode)
 		}
@@ -205,25 +212,29 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) {
 		AccountType: userapi.AccountTypeUser,
 	}
 
-	base, close := testrig.CreateBaseDendrite(t, dbType)
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	routers := httputil.NewRouters()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 	defer close()
+	natsInstance := jetstream.NATSInstance{}
 
-	jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-	defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
+	jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+	defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
 	// order is:
 	// m.room.create
 	// m.room.member
 	// m.room.power_levels
 	// m.room.join_rules
 	// m.room.history_visibility
-	msgs := toNATSMsgs(t, base, room.Events()...)
+	msgs := toNATSMsgs(t, cfg, room.Events()...)
 	sinceTokens := make([]string, len(msgs))
-	AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}})
+	AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, caches, caching.DisableMetrics)
 	for i, msg := range msgs {
 		testrig.MustPublishMsgs(t, jsctx, msg)
 		time.Sleep(100 * time.Millisecond)
 		w := httptest.NewRecorder()
-		base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
+		routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
 			"access_token": alice.AccessToken,
 			"timeout":      "0",
 		})))
@@ -253,7 +264,7 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) {
 	t.Logf("waited for events to be consumed; syncing with %v", sinceTokens)
 	for i, since := range sinceTokens {
 		w := httptest.NewRecorder()
-		base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
+		routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
 			"access_token": alice.AccessToken,
 			"timeout":      "0",
 			"since":        since,
@@ -295,16 +306,20 @@ func testSyncAPIUpdatePresenceImmediately(t *testing.T, dbType test.DBType) {
 		AccountType: userapi.AccountTypeUser,
 	}
 
-	base, close := testrig.CreateBaseDendrite(t, dbType)
-	base.Cfg.Global.Presence.EnableOutbound = true
-	base.Cfg.Global.Presence.EnableInbound = true
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	routers := httputil.NewRouters()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+	cfg.Global.Presence.EnableOutbound = true
+	cfg.Global.Presence.EnableInbound = true
 	defer close()
+	natsInstance := jetstream.NATSInstance{}
 
-	jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-	defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
-	AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{})
+	jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+	defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
+	AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, caches, caching.DisableMetrics)
 	w := httptest.NewRecorder()
-	base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
+	routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
 		"access_token": alice.AccessToken,
 		"timeout":      "0",
 		"set_presence": "online",
@@ -410,17 +425,20 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) {
 			userType = "real user"
 		}
 
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		routers := httputil.NewRouters()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 		defer close()
+		natsInstance := jetstream.NATSInstance{}
 
-		jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-		defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
+		jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+		defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
 
 		// Use the actual internal roomserver API
-		rsAPI := roomserver.NewInternalAPI(base)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 		rsAPI.SetFederationAPI(nil, nil)
-
-		AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{aliceDev, bobDev}}, rsAPI)
+		AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{aliceDev, bobDev}}, rsAPI, caches, caching.DisableMetrics)
 
 		for _, tc := range testCases {
 			testname := fmt.Sprintf("%s - %s", tc.historyVisibility, userType)
@@ -435,7 +453,7 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) {
 				if err := api.SendEvents(ctx, rsAPI, api.KindNew, eventsToSend, "test", "test", "test", nil, false); err != nil {
 					t.Fatalf("failed to send events: %v", err)
 				}
-				syncUntil(t, base, aliceDev.AccessToken, false,
+				syncUntil(t, routers, aliceDev.AccessToken, false,
 					func(syncBody string) bool {
 						path := fmt.Sprintf(`rooms.join.%s.timeline.events.#(content.body=="%s")`, room.ID, beforeJoinBody)
 						return gjson.Get(syncBody, path).Exists()
@@ -444,7 +462,7 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) {
 
 				// There is only one event, we expect only to be able to see this, if the room is world_readable
 				w := httptest.NewRecorder()
-				base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/messages", room.ID), test.WithQueryParams(map[string]string{
+				routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/messages", room.ID), test.WithQueryParams(map[string]string{
 					"access_token": bobDev.AccessToken,
 					"dir":          "b",
 					"filter":       `{"lazy_load_members":true}`, // check that lazy loading doesn't break history visibility
@@ -475,7 +493,7 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) {
 				if err := api.SendEvents(ctx, rsAPI, api.KindNew, eventsToSend, "test", "test", "test", nil, false); err != nil {
 					t.Fatalf("failed to send events: %v", err)
 				}
-				syncUntil(t, base, aliceDev.AccessToken, false,
+				syncUntil(t, routers, aliceDev.AccessToken, false,
 					func(syncBody string) bool {
 						path := fmt.Sprintf(`rooms.join.%s.timeline.events.#(content.body=="%s")`, room.ID, afterJoinBody)
 						return gjson.Get(syncBody, path).Exists()
@@ -484,7 +502,7 @@ func testHistoryVisibility(t *testing.T, dbType test.DBType) {
 
 				// Verify the messages after/before invite are visible or not
 				w = httptest.NewRecorder()
-				base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/messages", room.ID), test.WithQueryParams(map[string]string{
+				routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", fmt.Sprintf("/_matrix/client/v3/rooms/%s/messages", room.ID), test.WithQueryParams(map[string]string{
 					"access_token": bobDev.AccessToken,
 					"dir":          "b",
 				})))
@@ -710,17 +728,20 @@ func TestGetMembership(t *testing.T) {
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
 
-		base, close := testrig.CreateBaseDendrite(t, dbType)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		routers := httputil.NewRouters()
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
 		defer close()
-
-		jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-		defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
+		natsInstance := jetstream.NATSInstance{}
+		jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+		defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
 
 		// Use an actual roomserver for this
-		rsAPI := roomserver.NewInternalAPI(base)
+		rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 		rsAPI.SetFederationAPI(nil, nil)
 
-		AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{aliceDev, bobDev}}, rsAPI)
+		AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{aliceDev, bobDev}}, rsAPI, caches, caching.DisableMetrics)
 
 		for _, tc := range testCases {
 			t.Run(tc.name, func(t *testing.T) {
@@ -740,7 +761,7 @@ func TestGetMembership(t *testing.T) {
 				if tc.useSleep {
 					time.Sleep(time.Millisecond * 100)
 				} else {
-					syncUntil(t, base, aliceDev.AccessToken, false, func(syncBody string) bool {
+					syncUntil(t, routers, aliceDev.AccessToken, false, func(syncBody string) bool {
 						// wait for the last sent eventID to come down sync
 						path := fmt.Sprintf(`rooms.join.%s.timeline.events.#(event_id=="%s")`, room.ID, room.Events()[len(room.Events())-1].EventID())
 						return gjson.Get(syncBody, path).Exists()
@@ -748,7 +769,7 @@ func TestGetMembership(t *testing.T) {
 				}
 
 				w := httptest.NewRecorder()
-				base.PublicClientAPIMux.ServeHTTP(w, tc.request(t, room))
+				routers.Client.ServeHTTP(w, tc.request(t, room))
 				if w.Code != 200 && tc.wantOK {
 					t.Logf("%s", w.Body.String())
 					t.Fatalf("got HTTP %d want %d", w.Code, 200)
@@ -781,16 +802,19 @@ func testSendToDevice(t *testing.T, dbType test.DBType) {
 		AccountType: userapi.AccountTypeUser,
 	}
 
-	base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-	defer baseClose()
-
-	jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-	defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	routers := httputil.NewRouters()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+	defer close()
+	natsInstance := jetstream.NATSInstance{}
 
-	AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{})
+	jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+	defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
+	AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, caches, caching.DisableMetrics)
 
 	producer := producers.SyncAPIProducer{
-		TopicSendToDeviceEvent: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
+		TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
 		JetStream:              jsctx,
 	}
 
@@ -876,7 +900,7 @@ func testSendToDevice(t *testing.T, dbType test.DBType) {
 			}
 		}
 
-		syncUntil(t, base, alice.AccessToken,
+		syncUntil(t, routers, alice.AccessToken,
 			len(tc.want) == 0,
 			func(body string) bool {
 				return gjson.Get(body, fmt.Sprintf(`to_device.events.#(content.dummy=="message %d")`, msgCounter)).Exists()
@@ -885,7 +909,7 @@ func testSendToDevice(t *testing.T, dbType test.DBType) {
 
 		// Execute a /sync request, recording the response
 		w := httptest.NewRecorder()
-		base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
+		routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
 			"access_token": alice.AccessToken,
 			"since":        tc.since,
 		})))
@@ -999,14 +1023,18 @@ func testContext(t *testing.T, dbType test.DBType) {
 		AccountType: userapi.AccountTypeUser,
 	}
 
-	base, baseClose := testrig.CreateBaseDendrite(t, dbType)
-	defer baseClose()
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	routers := httputil.NewRouters()
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+	defer close()
 
 	// Use an actual roomserver for this
-	rsAPI := roomserver.NewInternalAPI(base)
+	natsInstance := jetstream.NATSInstance{}
+	rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
 	rsAPI.SetFederationAPI(nil, nil)
 
-	AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, rsAPI)
+	AddPublicRoutes(processCtx, routers, cfg, cm, &natsInstance, &syncUserAPI{accounts: []userapi.Device{alice}}, rsAPI, caches, caching.DisableMetrics)
 
 	room := test.NewRoom(t, user)
 
@@ -1019,10 +1047,10 @@ func testContext(t *testing.T, dbType test.DBType) {
 		t.Fatalf("failed to send events: %v", err)
 	}
 
-	jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
-	defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream)
+	jsctx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+	defer jetstream.DeleteAllStreams(jsctx, &cfg.Global.JetStream)
 
-	syncUntil(t, base, alice.AccessToken, false, func(syncBody string) bool {
+	syncUntil(t, routers, alice.AccessToken, false, func(syncBody string) bool {
 		// wait for the last sent eventID to come down sync
 		path := fmt.Sprintf(`rooms.join.%s.timeline.events.#(event_id=="%s")`, room.ID, thirdMsg.EventID())
 		return gjson.Get(syncBody, path).Exists()
@@ -1049,7 +1077,7 @@ func testContext(t *testing.T, dbType test.DBType) {
 					params[k] = v
 				}
 			}
-			base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", requestPath, test.WithQueryParams(params)))
+			routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", requestPath, test.WithQueryParams(params)))
 
 			if tc.wantError && w.Code == 200 {
 				t.Fatalf("Expected an error, but got none")
@@ -1137,9 +1165,10 @@ func TestUpdateRelations(t *testing.T) {
 	room := test.NewRoom(t, alice)
 
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		base, shutdownBase := testrig.CreateBaseDendrite(t, dbType)
-		t.Cleanup(shutdownBase)
-		db, err := storage.NewSyncServerDatasource(base, &base.Cfg.SyncAPI.Database)
+		cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		t.Cleanup(close)
+		db, err := storage.NewSyncServerDatasource(processCtx.Context(), cm, &cfg.SyncAPI.Database)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -1161,10 +1190,11 @@ func TestUpdateRelations(t *testing.T) {
 }
 
 func syncUntil(t *testing.T,
-	base *base.BaseDendrite, accessToken string,
+	routers httputil.Routers, accessToken string,
 	skip bool,
 	checkFunc func(syncBody string) bool,
 ) {
+	t.Helper()
 	if checkFunc == nil {
 		t.Fatalf("No checkFunc defined")
 	}
@@ -1178,7 +1208,7 @@ func syncUntil(t *testing.T,
 	go func() {
 		for {
 			w := httptest.NewRecorder()
-			base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
+			routers.Client.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{
 				"access_token": accessToken,
 				"timeout":      "1000",
 			})))
@@ -1196,14 +1226,14 @@ func syncUntil(t *testing.T,
 	}
 }
 
-func toNATSMsgs(t *testing.T, base *base.BaseDendrite, input ...*gomatrixserverlib.HeaderedEvent) []*nats.Msg {
+func toNATSMsgs(t *testing.T, cfg *config.Dendrite, input ...*gomatrixserverlib.HeaderedEvent) []*nats.Msg {
 	result := make([]*nats.Msg, len(input))
 	for i, ev := range input {
 		var addsStateIDs []string
 		if ev.StateKey() != nil {
 			addsStateIDs = append(addsStateIDs, ev.EventID())
 		}
-		result[i] = testrig.NewOutputEventMsg(t, base, ev.RoomID(), api.OutputEvent{
+		result[i] = testrig.NewOutputEventMsg(t, cfg, ev.RoomID(), api.OutputEvent{
 			Type: rsapi.OutputTypeNewRoomEvent,
 			NewRoomEvent: &rsapi.OutputNewRoomEvent{
 				Event:             ev,
diff --git a/test/testrig/base.go b/test/testrig/base.go
index bb8fded2..95370459 100644
--- a/test/testrig/base.go
+++ b/test/testrig/base.go
@@ -19,13 +19,12 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/config"
+	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/nats-io/nats.go"
 )
 
-func CreateBaseDendrite(t *testing.T, dbType test.DBType) (*base.BaseDendrite, func()) {
+func CreateConfig(t *testing.T, dbType test.DBType) (*config.Dendrite, *process.ProcessContext, func()) {
 	var cfg config.Dendrite
 	cfg.Defaults(config.DefaultOpts{
 		Generate:       false,
@@ -33,6 +32,7 @@ func CreateBaseDendrite(t *testing.T, dbType test.DBType) (*base.BaseDendrite, f
 	})
 	cfg.Global.JetStream.InMemory = true
 	cfg.FederationAPI.KeyPerspectives = nil
+	ctx := process.NewProcessContext()
 	switch dbType {
 	case test.DBTypePostgres:
 		cfg.Global.Defaults(config.DefaultOpts{ // autogen a signing key
@@ -51,18 +51,19 @@ func CreateBaseDendrite(t *testing.T, dbType test.DBType) (*base.BaseDendrite, f
 		// use a distinct prefix else concurrent postgres/sqlite runs will clash since NATS will use
 		// the file system event with InMemory=true :(
 		cfg.Global.JetStream.TopicPrefix = fmt.Sprintf("Test_%d_", dbType)
-		connStr, close := test.PrepareDBConnectionString(t, dbType)
+		cfg.SyncAPI.Fulltext.InMemory = true
+
+		connStr, closeDb := test.PrepareDBConnectionString(t, dbType)
 		cfg.Global.DatabaseOptions = config.DatabaseOptions{
 			ConnectionString:       config.DataSource(connStr),
 			MaxOpenConnections:     10,
 			MaxIdleConnections:     2,
 			ConnMaxLifetimeSeconds: 60,
 		}
-		base := base.NewBaseDendrite(&cfg, base.DisableMetrics)
-		return base, func() {
-			base.ShutdownDendrite()
-			base.WaitForShutdown()
-			close()
+		return &cfg, ctx, func() {
+			ctx.ShutdownDendrite()
+			ctx.WaitForShutdown()
+			closeDb()
 		}
 	case test.DBTypeSQLite:
 		cfg.Defaults(config.DefaultOpts{
@@ -70,7 +71,7 @@ func CreateBaseDendrite(t *testing.T, dbType test.DBType) (*base.BaseDendrite, f
 			SingleDatabase: false,
 		})
 		cfg.Global.ServerName = "test"
-
+		cfg.SyncAPI.Fulltext.InMemory = true
 		// use a distinct prefix else concurrent postgres/sqlite runs will clash since NATS will use
 		// the file system event with InMemory=true :(
 		cfg.Global.JetStream.TopicPrefix = fmt.Sprintf("Test_%d_", dbType)
@@ -86,30 +87,13 @@ func CreateBaseDendrite(t *testing.T, dbType test.DBType) (*base.BaseDendrite, f
 		cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(filepath.Join("file://", tempDir, "userapi.db"))
 		cfg.RelayAPI.Database.ConnectionString = config.DataSource(filepath.Join("file://", tempDir, "relayapi.db"))
 
-		base := base.NewBaseDendrite(&cfg, base.DisableMetrics)
-		return base, func() {
-			base.ShutdownDendrite()
-			base.WaitForShutdown()
+		return &cfg, ctx, func() {
+			ctx.ShutdownDendrite()
+			ctx.WaitForShutdown()
 			t.Cleanup(func() {}) // removes t.TempDir, where all database files are created
 		}
 	default:
 		t.Fatalf("unknown db type: %v", dbType)
 	}
-	return nil, nil
-}
-
-func Base(cfg *config.Dendrite) (*base.BaseDendrite, nats.JetStreamContext, *nats.Conn) {
-	if cfg == nil {
-		cfg = &config.Dendrite{}
-		cfg.Defaults(config.DefaultOpts{
-			Generate:       true,
-			SingleDatabase: false,
-		})
-	}
-	cfg.Global.JetStream.InMemory = true
-	cfg.SyncAPI.Fulltext.InMemory = true
-	cfg.FederationAPI.KeyPerspectives = nil
-	base := base.NewBaseDendrite(cfg, base.DisableMetrics)
-	js, jc := base.NATS.Prepare(base.ProcessContext, &cfg.Global.JetStream)
-	return base, js, jc
+	return &config.Dendrite{}, nil, func() {}
 }
diff --git a/test/testrig/jetstream.go b/test/testrig/jetstream.go
index b880eea4..5f15cfb3 100644
--- a/test/testrig/jetstream.go
+++ b/test/testrig/jetstream.go
@@ -4,10 +4,10 @@ import (
 	"encoding/json"
 	"testing"
 
+	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/nats-io/nats.go"
 
 	"github.com/matrix-org/dendrite/roomserver/api"
-	"github.com/matrix-org/dendrite/setup/base"
 	"github.com/matrix-org/dendrite/setup/jetstream"
 )
 
@@ -20,9 +20,9 @@ func MustPublishMsgs(t *testing.T, jsctx nats.JetStreamContext, msgs ...*nats.Ms
 	}
 }
 
-func NewOutputEventMsg(t *testing.T, base *base.BaseDendrite, roomID string, update api.OutputEvent) *nats.Msg {
+func NewOutputEventMsg(t *testing.T, cfg *config.Dendrite, roomID string, update api.OutputEvent) *nats.Msg {
 	t.Helper()
-	msg := nats.NewMsg(base.Cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent))
+	msg := nats.NewMsg(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent))
 	msg.Header.Set(jetstream.RoomEventType, string(update.Type))
 	msg.Header.Set(jetstream.RoomID, roomID)
 	var err error
diff --git a/userapi/consumers/roomserver_test.go b/userapi/consumers/roomserver_test.go
index bc5ae652..4827ad47 100644
--- a/userapi/consumers/roomserver_test.go
+++ b/userapi/consumers/roomserver_test.go
@@ -7,22 +7,22 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/matrix-org/dendrite/internal/pushrules"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/dendrite/userapi/storage"
 	userAPITypes "github.com/matrix-org/dendrite/userapi/types"
 )
 
 func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.UserDatabase, func()) {
-	base, baseclose := testrig.CreateBaseDendrite(t, dbType)
 	t.Helper()
 	connStr, close := test.PrepareDBConnectionString(t, dbType)
-	db, err := storage.NewUserDatabase(base, &config.DatabaseOptions{
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewUserDatabase(context.Background(), cm, &config.DatabaseOptions{
 		ConnectionString: config.DataSource(connStr),
 	}, "", 4, 0, 0, "")
 	if err != nil {
@@ -30,7 +30,6 @@ func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.UserDatabase,
 	}
 	return db, func() {
 		close()
-		baseclose()
 	}
 }
 
diff --git a/userapi/internal/device_list_update_test.go b/userapi/internal/device_list_update_test.go
index 868fc9be..c0965a2c 100644
--- a/userapi/internal/device_list_update_test.go
+++ b/userapi/internal/device_list_update_test.go
@@ -27,13 +27,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 
 	roomserver "github.com/matrix-org/dendrite/roomserver/api"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/setup/process"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/dendrite/userapi/storage"
 )
@@ -363,9 +363,9 @@ func TestDebounce(t *testing.T) {
 func mustCreateKeyserverDB(t *testing.T, dbType test.DBType) (storage.KeyDatabase, func()) {
 	t.Helper()
 
-	base, _, _ := testrig.Base(nil)
 	connStr, clearDB := test.PrepareDBConnectionString(t, dbType)
-	db, err := storage.NewKeyDatabase(base, &config.DatabaseOptions{ConnectionString: config.DataSource(connStr)})
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewKeyDatabase(cm, &config.DatabaseOptions{ConnectionString: config.DataSource(connStr)})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/userapi/internal/key_api_test.go b/userapi/internal/key_api_test.go
index fc7e7e0d..de2a6d2c 100644
--- a/userapi/internal/key_api_test.go
+++ b/userapi/internal/key_api_test.go
@@ -5,9 +5,9 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/dendrite/userapi/internal"
 	"github.com/matrix-org/dendrite/userapi/storage"
@@ -16,15 +16,14 @@ import (
 func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.KeyDatabase, func()) {
 	t.Helper()
 	connStr, close := test.PrepareDBConnectionString(t, dbType)
-	base, _, _ := testrig.Base(nil)
-	db, err := storage.NewKeyDatabase(base, &config.DatabaseOptions{
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewKeyDatabase(cm, &config.DatabaseOptions{
 		ConnectionString: config.DataSource(connStr),
 	})
 	if err != nil {
 		t.Fatalf("failed to create new user db: %v", err)
 	}
 	return db, func() {
-		base.Close()
 		close()
 	}
 }
diff --git a/userapi/storage/storage_test.go b/userapi/storage/storage_test.go
index f52e7e17..cf7c5144 100644
--- a/userapi/storage/storage_test.go
+++ b/userapi/storage/storage_test.go
@@ -9,6 +9,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/userapi/types"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -33,9 +34,9 @@ var (
 )
 
 func mustCreateUserDatabase(t *testing.T, dbType test.DBType) (storage.UserDatabase, func()) {
-	base, baseclose := testrig.CreateBaseDendrite(t, dbType)
 	connStr, close := test.PrepareDBConnectionString(t, dbType)
-	db, err := storage.NewUserDatabase(base, &config.DatabaseOptions{
+	cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+	db, err := storage.NewUserDatabase(context.Background(), cm, &config.DatabaseOptions{
 		ConnectionString: config.DataSource(connStr),
 	}, "localhost", bcrypt.MinCost, openIDLifetimeMS, loginTokenLifetime, "_server")
 	if err != nil {
@@ -43,7 +44,6 @@ func mustCreateUserDatabase(t *testing.T, dbType test.DBType) (storage.UserDatab
 	}
 	return db, func() {
 		close()
-		baseclose()
 	}
 }
 
@@ -576,8 +576,9 @@ func Test_Notification(t *testing.T) {
 }
 
 func mustCreateKeyDatabase(t *testing.T, dbType test.DBType) (storage.KeyDatabase, func()) {
-	base, close := testrig.CreateBaseDendrite(t, dbType)
-	db, err := storage.NewKeyDatabase(base, &base.Cfg.KeyServer.Database)
+	cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+	cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+	db, err := storage.NewKeyDatabase(cm, &cfg.KeyServer.Database)
 	if err != nil {
 		t.Fatalf("failed to create new database: %v", err)
 	}
diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go
index 01e491cb..03e65635 100644
--- a/userapi/userapi_test.go
+++ b/userapi/userapi_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/dendrite/userapi/producers"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
@@ -67,32 +68,25 @@ func MustMakeInternalAPI(t *testing.T, opts apiTestOpts, dbType test.DBType, pub
 	if opts.loginTokenLifetime == 0 {
 		opts.loginTokenLifetime = api.DefaultLoginTokenLifetime * time.Millisecond
 	}
-	base, baseclose := testrig.CreateBaseDendrite(t, dbType)
-	connStr, close := test.PrepareDBConnectionString(t, dbType)
+	cfg, ctx, close := testrig.CreateConfig(t, dbType)
 	sName := serverName
 	if opts.serverName != "" {
 		sName = gomatrixserverlib.ServerName(opts.serverName)
 	}
-	accountDB, err := storage.NewUserDatabase(base, &config.DatabaseOptions{
-		ConnectionString: config.DataSource(connStr),
-	}, sName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "")
+	cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
+
+	accountDB, err := storage.NewUserDatabase(ctx.Context(), cm, &cfg.UserAPI.AccountDatabase, sName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "")
 	if err != nil {
 		t.Fatalf("failed to create account DB: %s", err)
 	}
 
-	keyDB, err := storage.NewKeyDatabase(base, &config.DatabaseOptions{
-		ConnectionString: config.DataSource(connStr),
-	})
+	keyDB, err := storage.NewKeyDatabase(cm, &cfg.KeyServer.Database)
 	if err != nil {
 		t.Fatalf("failed to create key DB: %s", err)
 	}
 
-	cfg := &config.UserAPI{
-		Matrix: &config.Global{
-			SigningIdentity: gomatrixserverlib.SigningIdentity{
-				ServerName: sName,
-			},
-		},
+	cfg.Global.SigningIdentity = gomatrixserverlib.SigningIdentity{
+		ServerName: sName,
 	}
 
 	if publisher == nil {
@@ -104,12 +98,11 @@ func MustMakeInternalAPI(t *testing.T, opts apiTestOpts, dbType test.DBType, pub
 	return &internal.UserInternalAPI{
 			DB:                accountDB,
 			KeyDatabase:       keyDB,
-			Config:            cfg,
+			Config:            &cfg.UserAPI,
 			SyncProducer:      syncProducer,
 			KeyChangeProducer: keyChangeProducer,
 		}, accountDB, func() {
 			close()
-			baseclose()
 		}
 }
 
diff --git a/userapi/util/notify_test.go b/userapi/util/notify_test.go
index 421852d3..69461ddd 100644
--- a/userapi/util/notify_test.go
+++ b/userapi/util/notify_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"github.com/matrix-org/gomatrixserverlib"
 	"github.com/matrix-org/util"
 	"golang.org/x/crypto/bcrypt"
@@ -15,7 +16,6 @@ import (
 	"github.com/matrix-org/dendrite/internal/pushgateway"
 	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/test"
-	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/dendrite/userapi/api"
 	"github.com/matrix-org/dendrite/userapi/storage"
 	userUtil "github.com/matrix-org/dendrite/userapi/util"
@@ -77,9 +77,8 @@ func TestNotifyUserCountsAsync(t *testing.T) {
 		// Create DB and Dendrite base
 		connStr, close := test.PrepareDBConnectionString(t, dbType)
 		defer close()
-		base, _, _ := testrig.Base(nil)
-		defer base.Close()
-		db, err := storage.NewUserDatabase(base, &config.DatabaseOptions{
+		cm := sqlutil.NewConnectionManager(nil, config.DatabaseOptions{})
+		db, err := storage.NewUserDatabase(ctx, cm, &config.DatabaseOptions{
 			ConnectionString: config.DataSource(connStr),
 		}, "test", bcrypt.MinCost, 0, 0, "")
 		if err != nil {
diff --git a/userapi/util/phonehomestats_test.go b/userapi/util/phonehomestats_test.go
index 5f626b5b..191a35c0 100644
--- a/userapi/util/phonehomestats_test.go
+++ b/userapi/util/phonehomestats_test.go
@@ -7,10 +7,10 @@ import (
 	"testing"
 	"time"
 
+	"github.com/matrix-org/dendrite/internal/sqlutil"
 	"golang.org/x/crypto/bcrypt"
 
 	"github.com/matrix-org/dendrite/internal"
-	"github.com/matrix-org/dendrite/setup/config"
 	"github.com/matrix-org/dendrite/test"
 	"github.com/matrix-org/dendrite/test/testrig"
 	"github.com/matrix-org/dendrite/userapi/storage"
@@ -18,12 +18,10 @@ import (
 
 func TestCollect(t *testing.T) {
 	test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
-		b, _, _ := testrig.Base(nil)
-		connStr, closeDB := test.PrepareDBConnectionString(t, dbType)
+		cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
 		defer closeDB()
-		db, err := storage.NewUserDatabase(b, &config.DatabaseOptions{
-			ConnectionString: config.DataSource(connStr),
-		}, "localhost", bcrypt.MinCost, 1000, 1000, "")
+		cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+		db, err := storage.NewUserDatabase(processCtx.Context(), cm, &cfg.UserAPI.AccountDatabase, "localhost", bcrypt.MinCost, 1000, 1000, "")
 		if err != nil {
 			t.Error(err)
 		}
@@ -62,12 +60,12 @@ func TestCollect(t *testing.T) {
 		}))
 		defer srv.Close()
 
-		b.Cfg.Global.ReportStats.Endpoint = srv.URL
+		cfg.Global.ReportStats.Endpoint = srv.URL
 		stats := phoneHomeStats{
 			prevData:   timestampToRUUsage{},
 			serverName: "localhost",
 			startTime:  time.Now(),
-			cfg:        b.Cfg,
+			cfg:        cfg,
 			db:         db,
 			isMonolith: false,
 			client:     &http.Client{Timeout: time.Second},