diff --git a/cmd/root.go b/cmd/root.go
index 0963c7e59..3015dd752 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -83,6 +83,7 @@ func startServer(ctx context.Context) func() error {
 		a := CreateServer(conf.Server.MusicFolder)
 		a.MountRouter("Native API", consts.URLPathNativeAPI, CreateNativeAPIRouter())
 		a.MountRouter("Subsonic API", consts.URLPathSubsonicAPI, CreateSubsonicAPIRouter())
+		a.MountRouter("Public Endpoints", "/p", CreatePublicRouter())
 		if conf.Server.LastFM.Enabled {
 			a.MountRouter("LastFM Auth", consts.URLPathNativeAPI+"/lastfm", CreateLastFMRouter())
 		}
diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 49600c84a..5444271dd 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -21,6 +21,7 @@ import (
 	"github.com/navidrome/navidrome/server"
 	"github.com/navidrome/navidrome/server/events"
 	"github.com/navidrome/navidrome/server/nativeapi"
+	"github.com/navidrome/navidrome/server/public"
 	"github.com/navidrome/navidrome/server/subsonic"
 	"sync"
 )
@@ -63,6 +64,16 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
 	return router
 }
 
+func CreatePublicRouter() *public.Router {
+	sqlDB := db.Db()
+	dataStore := persistence.New(sqlDB)
+	fileCache := artwork.GetImageCache()
+	fFmpeg := ffmpeg.New()
+	artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg)
+	router := public.New(artworkArtwork)
+	return router
+}
+
 func CreateLastFMRouter() *lastfm.Router {
 	sqlDB := db.Db()
 	dataStore := persistence.New(sqlDB)
@@ -92,7 +103,7 @@ func createScanner() scanner.Scanner {
 
 // wire_injectors.go:
 
-var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
+var allProviders = wire.NewSet(core.Set, artwork.Set, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, db.Db)
 
 // Scanner must be a Singleton
 var (
diff --git a/cmd/wire_injectors.go b/cmd/wire_injectors.go
index a83881c55..cc896421f 100644
--- a/cmd/wire_injectors.go
+++ b/cmd/wire_injectors.go
@@ -16,6 +16,7 @@ import (
 	"github.com/navidrome/navidrome/server"
 	"github.com/navidrome/navidrome/server/events"
 	"github.com/navidrome/navidrome/server/nativeapi"
+	"github.com/navidrome/navidrome/server/public"
 	"github.com/navidrome/navidrome/server/subsonic"
 )
 
@@ -24,6 +25,7 @@ var allProviders = wire.NewSet(
 	artwork.Set,
 	subsonic.New,
 	nativeapi.New,
+	public.New,
 	persistence.New,
 	lastfm.NewRouter,
 	listenbrainz.NewRouter,
@@ -51,6 +53,12 @@ func CreateSubsonicAPIRouter() *subsonic.Router {
 	))
 }
 
+func CreatePublicRouter() *public.Router {
+	panic(wire.Build(
+		allProviders,
+	))
+}
+
 func CreateLastFMRouter() *lastfm.Router {
 	panic(wire.Build(
 		allProviders,
diff --git a/core/auth/auth.go b/core/auth/auth.go
index a92d95952..24ee2e732 100644
--- a/core/auth/auth.go
+++ b/core/auth/auth.go
@@ -32,10 +32,25 @@ func Init(ds model.DataStore) {
 	})
 }
 
+func createBaseClaims() map[string]any {
+	tokenClaims := map[string]any{}
+	tokenClaims[jwt.IssuerKey] = consts.JWTIssuer
+	tokenClaims[jwt.IssuedAtKey] = time.Now().UTC().Unix()
+	return tokenClaims
+}
+
+func CreatePublicToken(claims map[string]any) (string, error) {
+	tokenClaims := createBaseClaims()
+	for k, v := range claims {
+		tokenClaims[k] = v
+	}
+	_, token, err := TokenAuth.Encode(tokenClaims)
+
+	return token, err
+}
+
 func CreateToken(u *model.User) (string, error) {
-	claims := map[string]interface{}{}
-	claims[jwt.IssuerKey] = consts.JWTIssuer
-	claims[jwt.IssuedAtKey] = time.Now().UTC().Unix()
+	claims := createBaseClaims()
 	claims[jwt.SubjectKey] = u.UserName
 	claims["uid"] = u.ID
 	claims["adm"] = u.IsAdmin
diff --git a/server/auth.go b/server/auth.go
index 138ca121f..c4c23ed07 100644
--- a/server/auth.go
+++ b/server/auth.go
@@ -107,8 +107,6 @@ func getCredentialsFromBody(r *http.Request) (username string, password string,
 }
 
 func createAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
-	auth.Init(ds)
-
 	return func(w http.ResponseWriter, r *http.Request) {
 		username, password, err := getCredentialsFromBody(r)
 		if err != nil {
diff --git a/server/middlewares.go b/server/middlewares.go
index 1c374fa2b..624803f69 100644
--- a/server/middlewares.go
+++ b/server/middlewares.go
@@ -5,9 +5,11 @@ import (
 	"fmt"
 	"io/fs"
 	"net/http"
+	"net/url"
 	"strings"
 	"time"
 
+	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/cors"
 
 	"github.com/go-chi/chi/v5/middleware"
@@ -199,3 +201,26 @@ func firstOr(or string, strings ...string) string {
 	}
 	return or
 }
+
+// URLParamsMiddleware convert Chi URL params (from Context) to query params, as expected by our REST package
+func URLParamsMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		ctx := chi.RouteContext(r.Context())
+		parts := make([]string, 0)
+		for i, key := range ctx.URLParams.Keys {
+			value := ctx.URLParams.Values[i]
+			if key == "*" {
+				continue
+			}
+			parts = append(parts, url.QueryEscape(":"+key)+"="+url.QueryEscape(value))
+		}
+		q := strings.Join(parts, "&")
+		if r.URL.RawQuery == "" {
+			r.URL.RawQuery = q
+		} else {
+			r.URL.RawQuery += "&" + q
+		}
+
+		next.ServeHTTP(w, r)
+	})
+}
diff --git a/server/nativeapi/native_api.go b/server/nativeapi/native_api.go
index 083bccf50..fb53c4e4d 100644
--- a/server/nativeapi/native_api.go
+++ b/server/nativeapi/native_api.go
@@ -3,8 +3,6 @@ package nativeapi
 import (
 	"context"
 	"net/http"
-	"net/url"
-	"strings"
 
 	"github.com/deluan/rest"
 	"github.com/go-chi/chi/v5"
@@ -77,7 +75,7 @@ func (n *Router) RX(r chi.Router, pathPrefix string, constructor rest.Repository
 			r.Post("/", rest.Post(constructor))
 		}
 		r.Route("/{id}", func(r chi.Router) {
-			r.Use(urlParams)
+			r.Use(server.URLParamsMiddleware)
 			r.Get("/", rest.Get(constructor))
 			if persistable {
 				r.Put("/", rest.Put(constructor))
@@ -92,7 +90,7 @@ func (n *Router) addPlaylistTrackRoute(r chi.Router) {
 		r.Get("/", func(w http.ResponseWriter, r *http.Request) {
 			getPlaylist(n.ds)(w, r)
 		})
-		r.With(urlParams).Route("/", func(r chi.Router) {
+		r.With(server.URLParamsMiddleware).Route("/", func(r chi.Router) {
 			r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
 				deleteFromPlaylist(n.ds)(w, r)
 			})
@@ -101,7 +99,7 @@ func (n *Router) addPlaylistTrackRoute(r chi.Router) {
 			})
 		})
 		r.Route("/{id}", func(r chi.Router) {
-			r.Use(urlParams)
+			r.Use(server.URLParamsMiddleware)
 			r.Put("/", func(w http.ResponseWriter, r *http.Request) {
 				reorderItem(n.ds)(w, r)
 			})
@@ -111,26 +109,3 @@ func (n *Router) addPlaylistTrackRoute(r chi.Router) {
 		})
 	})
 }
-
-// Middleware to convert Chi URL params (from Context) to query params, as expected by our REST package
-func urlParams(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		ctx := chi.RouteContext(r.Context())
-		parts := make([]string, 0)
-		for i, key := range ctx.URLParams.Keys {
-			value := ctx.URLParams.Values[i]
-			if key == "*" {
-				continue
-			}
-			parts = append(parts, url.QueryEscape(":"+key)+"="+url.QueryEscape(value))
-		}
-		q := strings.Join(parts, "&")
-		if r.URL.RawQuery == "" {
-			r.URL.RawQuery = q
-		} else {
-			r.URL.RawQuery += "&" + q
-		}
-
-		next.ServeHTTP(w, r)
-	})
-}
diff --git a/server/public/public_endpoints.go b/server/public/public_endpoints.go
new file mode 100644
index 000000000..20fb62c6a
--- /dev/null
+++ b/server/public/public_endpoints.go
@@ -0,0 +1,109 @@
+package public
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-chi/jwtauth/v5"
+	"github.com/lestrrat-go/jwx/v2/jwt"
+	"github.com/navidrome/navidrome/core/artwork"
+	"github.com/navidrome/navidrome/core/auth"
+	"github.com/navidrome/navidrome/log"
+	"github.com/navidrome/navidrome/model"
+	"github.com/navidrome/navidrome/server"
+)
+
+type Router struct {
+	http.Handler
+	artwork artwork.Artwork
+}
+
+func New(artwork artwork.Artwork) *Router {
+	p := &Router{artwork: artwork}
+	p.Handler = p.routes()
+
+	t, err := auth.CreatePublicToken(map[string]any{
+		"id":   "al-ee07551e7371500da55e23ae8520f1d8",
+		"size": 300,
+	})
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println("!!!!!!!!!!!!!!!!!", t, "!!!!!!!!!!!!!!!!")
+
+	return p
+}
+
+func (p *Router) routes() http.Handler {
+	r := chi.NewRouter()
+
+	r.Group(func(r chi.Router) {
+		r.Use(server.URLParamsMiddleware)
+		r.Use(jwtVerifier)
+		r.Use(validator)
+		r.Get("/img/{jwt}", p.handleImages)
+	})
+	return r
+}
+
+func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
+	_, claims, _ := jwtauth.FromContext(r.Context())
+	id, ok := claims["id"].(string)
+	if !ok {
+		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+		return
+	}
+	size, ok := claims["size"].(float64)
+	if !ok {
+		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+		return
+	}
+	imgReader, lastUpdate, err := p.artwork.Get(r.Context(), id, int(size))
+	w.Header().Set("cache-control", "public, max-age=315360000")
+	w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123))
+
+	switch {
+	case errors.Is(err, context.Canceled):
+		return
+	case errors.Is(err, model.ErrNotFound):
+		log.Error(r, "Couldn't find coverArt", "id", id, err)
+		http.Error(w, "Artwork not found", http.StatusNotFound)
+		return
+	case err != nil:
+		log.Error(r, "Error retrieving coverArt", "id", id, err)
+		http.Error(w, "Error retrieving coverArt", http.StatusInternalServerError)
+		return
+	}
+
+	defer imgReader.Close()
+	_, err = io.Copy(w, imgReader)
+}
+
+func jwtVerifier(next http.Handler) http.Handler {
+	return jwtauth.Verify(auth.TokenAuth, func(r *http.Request) string {
+		return r.URL.Query().Get(":jwt")
+	})(next)
+}
+
+func validator(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		token, _, err := jwtauth.FromContext(r.Context())
+
+		validErr := jwt.Validate(token,
+			jwt.WithRequiredClaim("id"),
+			jwt.WithRequiredClaim("size"),
+		)
+		if err != nil || token == nil || validErr != nil {
+			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+			return
+		}
+
+		// Token is authenticated, pass it through
+		next.ServeHTTP(w, r)
+	})
+}
diff --git a/server/server.go b/server/server.go
index f96b1c3d3..7f2029b15 100644
--- a/server/server.go
+++ b/server/server.go
@@ -28,6 +28,7 @@ type Server struct {
 
 func New(ds model.DataStore) *Server {
 	s := &Server{ds: ds}
+	auth.Init(s.ds)
 	initialSetup(ds)
 	s.initRoutes()
 	checkFfmpegInstallation()
@@ -80,8 +81,6 @@ func (s *Server) Run(ctx context.Context, addr string) error {
 }
 
 func (s *Server) initRoutes() {
-	auth.Init(s.ds)
-
 	s.appRoot = path.Join(conf.Server.BaseURL, consts.URLPathUI)
 
 	r := chi.NewRouter()