diff --git a/pgs/cli.go b/pgs/cli.go
index 267a5364..5e93926c 100644
--- a/pgs/cli.go
+++ b/pgs/cli.go
@@ -52,7 +52,7 @@ func projectTable(styles common.Styles, projects []*db.Project, width int) *tabl
 }
 
 func getHelpText(styles common.Styles, userName string, width int) string {
-	helpStr := "Commands: [help, stats, ls, rm, link, unlink, prune, retain, depends, acl]\n"
+	helpStr := "Commands: [help, stats, ls, rm, link, unlink, prune, retain, depends, acl, cache]\n"
 	helpStr += styles.Note.Render("NOTICE:") + " *must* append with `--write` for the changes to persist.\n"
 
 	projectName := "projA"
@@ -98,6 +98,10 @@ func getHelpText(styles common.Styles, userName string, width int) string {
 			fmt.Sprintf("acl %s", projectName),
 			fmt.Sprintf("access control for `%s`", projectName),
 		},
+		{
+			fmt.Sprintf("cache %s", projectName),
+			fmt.Sprintf("clear http cache for `%s`", projectName),
+		},
 	}
 
 	t := table.New().
@@ -120,6 +124,7 @@ type Cmd struct {
 	Styles  common.Styles
 	Width   int
 	Height  int
+	Cfg     *shared.ConfigSite
 }
 
 func (c *Cmd) output(out string) {
@@ -484,3 +489,34 @@ func (c *Cmd) acl(projectName, aclType string, acls []string) error {
 	}
 	return nil
 }
+
+func (c *Cmd) cache(projectName string) error {
+	c.Log.Info(
+		"user running `cache` command",
+		"user", c.User.Name,
+		"project", projectName,
+	)
+	c.output(fmt.Sprintf("clearing http cache for %s", projectName))
+	if c.Write {
+		surrogate := getSurrogateKey(c.User.Name, projectName)
+		return purgeCache(c.Cfg, surrogate)
+	}
+	return nil
+}
+
+func (c *Cmd) cacheAll() error {
+	isAdmin := c.Dbpool.HasFeatureForUser(c.User.ID, "admin")
+	if !isAdmin {
+		return fmt.Errorf("must be admin to use this command")
+	}
+
+	c.Log.Info(
+		"admin running `cache-all` command",
+		"user", c.User.Name,
+	)
+	c.output("clearing http cache for all sites")
+	if c.Write {
+		return purgeAllCache(c.Cfg)
+	}
+	return nil
+}
diff --git a/pgs/uploader.go b/pgs/uploader.go
index bd1f6bc8..44f9aa46 100644
--- a/pgs/uploader.go
+++ b/pgs/uploader.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"io/fs"
 	"log/slog"
-	"net/http"
 	"os"
 	"path"
 	"path/filepath"
@@ -412,7 +411,9 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (s
 		utils.BytesToGB(maxSize),
 		(float32(nextStorageSize)/float32(maxSize))*100,
 	)
-	h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName)
+
+	surrogate := getSurrogateKey(user.Name, projectName)
+	h.CacheClearingQueue <- surrogate
 
 	return str, nil
 }
@@ -480,7 +481,10 @@ func (h *UploadAssetHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) e
 		}
 	}
 	err = h.Storage.DeleteObject(bucket, assetFilepath)
-	h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName)
+
+	surrogate := getSurrogateKey(user.Name, projectName)
+	h.CacheClearingQueue <- surrogate
+
 	return err
 }
 
@@ -533,7 +537,6 @@ func (h *UploadAssetHandler) writeAsset(reader io.Reader, data *FileData) (int64
 // Repeated messages for the same site are grouped so that we only flush once
 // per site per 5 seconds.
 func runCacheQueue(ch chan string, cfg *shared.ConfigSite) {
-	cacheApiUrl := fmt.Sprintf("https://%s/souin-api/souin/", cfg.Domain)
 	var pendingFlushes sync.Map
 	tick := time.Tick(5 * time.Second)
 	for {
@@ -544,7 +547,7 @@ func runCacheQueue(ch chan string, cfg *shared.ConfigSite) {
 			go func() {
 				pendingFlushes.Range(func(key, value any) bool {
 					pendingFlushes.Delete(key)
-					err := purgeCache(key.(string), cacheApiUrl, cfg.CacheUser, cfg.CachePassword)
+					err := purgeCache(cfg, key.(string))
 					if err != nil {
 						cfg.Logger.Error("failed to clear cache", "err", err.Error())
 					}
@@ -554,29 +557,3 @@ func runCacheQueue(ch chan string, cfg *shared.ConfigSite) {
 		}
 	}
 }
-
-// purgeCache send an HTTP request to the pgs Caddy instance which purges
-// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a
-// "surrogate-key: <subdomain>" header on every pgs response which ensures all
-// cached assets for a given subdomain are grouped under a single key (which is
-// separate from the "GET-https-example.com-/path" key used for serving files
-// from the cache).
-func purgeCache(subdomain string, cacheApiUrl string, username string, password string) error {
-	client := &http.Client{
-		Timeout: time.Second * 5,
-	}
-	req, err := http.NewRequest("PURGE", cacheApiUrl, nil)
-	if err != nil {
-		return err
-	}
-	req.Header.Add("Surrogate-Key", subdomain)
-	req.SetBasicAuth(username, password)
-	resp, err := client.Do(req)
-	if err != nil {
-		return err
-	}
-	if resp.StatusCode != 204 {
-		return fmt.Errorf("received unexpected response code %d", resp.StatusCode)
-	}
-	return nil
-}
diff --git a/pgs/web_cache.go b/pgs/web_cache.go
new file mode 100644
index 00000000..d6b15c46
--- /dev/null
+++ b/pgs/web_cache.go
@@ -0,0 +1,51 @@
+package pgs
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/picosh/pico/shared"
+)
+
+func getSurrogateKey(userName, projectName string) string {
+	return fmt.Sprintf("%s-%s", userName, projectName)
+}
+
+func getCacheApiUrl(cfg *shared.ConfigSite) string {
+	return fmt.Sprintf("%s://%s/souin-api/souin/", cfg.Protocol, cfg.Domain)
+}
+
+// purgeCache send an HTTP request to the pgs Caddy instance which purges
+// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a
+// "surrogate-key: <subdomain>" header on every pgs response which ensures all
+// cached assets for a given subdomain are grouped under a single key (which is
+// separate from the "GET-https-example.com-/path" key used for serving files
+// from the cache).
+func purgeCache(cfg *shared.ConfigSite, surrogate string) error {
+	cacheApiUrl := getCacheApiUrl(cfg)
+	cfg.Logger.Info("purging cache", "url", cacheApiUrl, "surrogate", surrogate)
+	client := &http.Client{
+		Timeout: time.Second * 5,
+	}
+	req, err := http.NewRequest("PURGE", cacheApiUrl, nil)
+	if err != nil {
+		return err
+	}
+	if surrogate != "" {
+		req.Header.Add("Surrogate-Key", surrogate)
+	}
+	req.SetBasicAuth(cfg.CacheUser, cfg.CachePassword)
+	resp, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode != 204 {
+		return fmt.Errorf("received unexpected response code %d", resp.StatusCode)
+	}
+	return nil
+}
+
+func purgeAllCache(cfg *shared.ConfigSite) error {
+	return purgeCache(cfg, "")
+}
diff --git a/pgs/wish.go b/pgs/wish.go
index e0771369..5a569089 100644
--- a/pgs/wish.go
+++ b/pgs/wish.go
@@ -106,6 +106,7 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 				Styles:  styles,
 				Width:   width,
 				Height:  height,
+				Cfg:     handler.Cfg,
 			}
 
 			cmd := strings.TrimSpace(args[0])
@@ -121,6 +122,12 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 					err := opts.ls()
 					opts.bail(err)
 					return
+				} else if cmd == "cache-all" {
+					opts.Write = true
+					err := opts.cacheAll()
+					opts.notice()
+					opts.bail(err)
+					return
 				} else {
 					next(sesh)
 					return
@@ -212,6 +219,17 @@ func WishMiddleware(handler *UploadAssetHandler) wish.Middleware {
 				opts.notice()
 				opts.bail(err)
 				return
+			} else if cmd == "cache" {
+				cacheCmd, write := flagSet("cache", sesh)
+				if !flagCheck(cacheCmd, projectName, cmdArgs) {
+					return
+				}
+				opts.Write = *write
+
+				err := opts.cache(projectName)
+				opts.notice()
+				opts.bail(err)
+				return
 			} else if cmd == "acl" {
 				aclCmd, write := flagSet("acl", sesh)
 				aclType := aclCmd.String("type", "", "access type: public, pico, pubkeys")