diff --git a/internal/subscriptions/subscriptions.go b/internal/subscriptions/subscriptions.go
new file mode 100644
index 000000000..90d5609a5
--- /dev/null
+++ b/internal/subscriptions/subscriptions.go
@@ -0,0 +1,61 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package subscriptions
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+)
+
+type Subscriptions struct {
+ state *state.State
+}
+
+func (s *Subscriptions) UpdateDomainPermissions(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+) {
+ l := log.
+ WithContext(ctx).
+ WithField("permType", permType.String())
+ l.Info("start")
+
+ // Get permission subscriptions in priority order (highest -> lowest).
+ permSubs, err := s.state.DB.GetDomainPermissionSubscriptionsByPriority(ctx, permType)
+ if err != nil {
+ l.Error(err)
+ return
+ }
+
+ if len(permSubs) == 0 {
+ // Nothing to do.
+ return
+ }
+
+ for i, permSub := range permSubs {
+
+ // Slice of permission subscriptions that
+ // have a higher priority than this one.
+ higherPrios := permSubs[:i]
+
+
+ }
+}
diff --git a/internal/transport/derefdomainpermlist.go b/internal/transport/derefdomainpermlist.go
new file mode 100644
index 000000000..045c69a26
--- /dev/null
+++ b/internal/transport/derefdomainpermlist.go
@@ -0,0 +1,99 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package transport
+
+import (
+ "context"
+ "io"
+ "net/http"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+type DomainPermissionsRespRaw struct {
+ body []byte
+}
+
+func (t *transport) DereferenceDomainPermissions(
+ ctx context.Context,
+ permSub *gtsmodel.DomainPermissionSubscription,
+ useCacheHeaders bool,
+) (*DomainPermissionsRespRaw, error) {
+ // Prepare new HTTP request to endpoint
+ req, err := http.NewRequestWithContext(ctx, "GET", permSub.URI, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set basic auth header if necessary.
+ if permSub.FetchUsername != "" || permSub.FetchPassword != "" {
+ req.SetBasicAuth(permSub.FetchUsername, permSub.FetchPassword)
+ }
+
+ // Set relevant Accept headers.
+ // Allow fallback in case target doesn't
+ // negotiate content type correctly.
+ req.Header.Add("Accept-Charset", "utf-8")
+ req.Header.Add("Accept", permSub.ContentType.String()+","+"*/*")
+
+ if useCacheHeaders {
+ // If we've successfully fetched this list
+ // before, set If-Modified-Since to last
+ // success to make the request conditional.
+ //
+ // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
+ if !permSub.SuccessfullyFetchedAt.IsZero() {
+ timeStr := permSub.SuccessfullyFetchedAt.Format(http.TimeFormat)
+ req.Header.Add("If-Modified-Since", timeStr)
+ }
+
+ // If we've got an ETag stored for this list, set
+ // If-None-Match to make the request conditional.
+ // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#caching_of_unchanged_resources.
+ if len(permSub.ETag) != 0 {
+ req.Header.Add("If-None-Match", permSub.ETag)
+ }
+ }
+
+ // Perform the HTTP request
+ rsp, err := t.GET(req)
+ if err != nil {
+ return nil, err
+ }
+ defer rsp.Body.Close()
+
+ // Read the body regardless of response code,
+ // as we may want to store any error message.
+ bytes, err := io.ReadAll(rsp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if rsp.StatusCode == http.StatusNotModified {
+ // Nothing has changed on the remote side since
+ // we last fetched, so there's nothing to do.
+ return nil, nil
+ }
+
+ // Ensure a non-error status response.
+ if rsp.StatusCode != http.StatusOK {
+ err := gtserror.NewFromResponse(rsp)
+ return nil, err
+ }
+}