diff --git a/main.go b/main.go
index 3ab761d522a6573a8d1434a6096fede430e75dc0..f6001029682b5d9eeb43060492c7410ea05110a2 100644
--- a/main.go
+++ b/main.go
@@ -31,6 +31,7 @@ func main() {
 	http.HandleFunc("/api/apps.json", s.handleApps)
 	http.HandleFunc("/api/releases.json", s.handleReleases)
 	http.HandleFunc("/api/release/", s.handleReleaseMirror)
+	http.HandleFunc("/api/apps/zip/", s.handleAppZip)
 	log.Printf("Listening on %s...", flagListen)
 	http.ListenAndServe(flagListen, nil)
 }
diff --git a/server_apps.go b/server_apps.go
index 8d9692fbd1b6446bde113b83e7e314844cece804..d39377d5cedaa34b85ab99fca070739cd0727df8 100644
--- a/server_apps.go
+++ b/server_apps.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"archive/zip"
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -35,6 +37,9 @@ type appInfo struct {
 	version     int
 	commit      string
 	stars       int
+
+	commitObj *object.Commit
+	zip       []byte
 }
 
 type GLProject struct {
@@ -72,6 +77,57 @@ func (s *server) getStars(ctx context.Context, repo string) (int, error) {
 	return project.StarCount, nil
 }
 
+func (s *server) zipApp(ctx context.Context, name, pathInRepo, repo string, obj *object.Commit) ([]byte, error) {
+	fi, err := obj.Files()
+	if err != nil {
+		return nil, fmt.Errorf("listing files: %w", err)
+	}
+	buf := bytes.NewBuffer(nil)
+	w := zip.NewWriter(buf)
+	for {
+		f, err := fi.Next()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, fmt.Errorf("fi.Next: %w", err)
+		}
+		prefix := ""
+		if pathInRepo != "" {
+			prefix = pathInRepo + "/"
+		}
+		if !strings.HasPrefix(f.Name, prefix) {
+			continue
+		}
+		if !f.Mode.IsFile() {
+			continue
+		}
+		p := f.Name[len(prefix):]
+		prefix = strings.ReplaceAll(repo, "/", "-")
+		outPath := path.Join(prefix, p)
+		fo, err := w.Create(outPath)
+		if err != nil {
+			return nil, fmt.Errorf("Create(%q): %w", outPath, err)
+		}
+		if f.Size+int64(buf.Len()) > 10<<20 {
+			return nil, fmt.Errorf("archive too large")
+		}
+		rdr, err := f.Blob.Reader()
+		if err != nil {
+			return nil, fmt.Errorf("Blob.Reader: %w", err)
+		}
+		_, err = io.Copy(fo, rdr)
+		rdr.Close()
+		if err != nil {
+			return nil, fmt.Errorf("when copying: %w", err)
+		}
+	}
+	if err := w.Close(); err != nil {
+		return nil, fmt.Errorf("when closing: %w", err)
+	}
+	return buf.Bytes(), nil
+}
+
 func (s *server) parseAppToml(ctx context.Context, pathInRepo string, obj *object.Commit) (*appInfo, error) {
 	p := path.Join(pathInRepo, "flow3r.toml")
 	f, err := obj.File(p)
@@ -132,6 +188,7 @@ func (s *server) parseAppToml(ctx context.Context, pathInRepo string, obj *objec
 		description: data.Metadata.Description,
 		version:     data.Metadata.Version,
 		commit:      obj.Hash.String(),
+		commitObj:   obj,
 	}, nil
 }
 
@@ -204,8 +261,14 @@ func (s *server) getAppInfo(ctx context.Context, pathInRepo, repo string) (*appI
 	if err != nil {
 		return nil, fmt.Errorf("getting stars failed: %w", err)
 	}
-	firstTime[highestVer].stars = stars
-	return firstTime[highestVer], nil
+	app := firstTime[highestVer]
+	app.stars = stars
+	zbytes, err := s.zipApp(ctx, app.name, pathInRepo, repo, app.commitObj)
+	if err != nil {
+		return nil, fmt.Errorf("zipping failed: %w", err)
+	}
+	app.zip = zbytes
+	return app, nil
 }
 
 func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
@@ -276,7 +339,6 @@ func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
 			log.Printf("App %q: couldn't parse TOML: %v", m[1], err)
 			continue
 		}
-		log.Printf("%+v", dat)
 		n := reAppRepo.FindStringSubmatch(dat.Repo)
 		if n == nil {
 			log.Printf("App %q: invalid repo %q", m[1], dat.Repo)
@@ -335,13 +397,10 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) {
 			continue
 		}
 		// Guarenteed to be exactly 2 parts because of the regex
-		parts := strings.Split(a.repository, "/")
-		orga := parts[0]
-		repo := parts[1]
 		resp.Apps = append(resp.Apps, app{
 			RepoURL:     "https://git.flow3r.garden/" + a.repository,
 			Commit:      a.appInfo.commit,
-			DownloadURL: fmt.Sprintf("https://git.flow3r.garden/%s/%s/-/archive/%s/%s-%s.zip", orga, repo, a.appInfo.commit, repo, a.appInfo.commit),
+			DownloadURL: fmt.Sprintf("%sapps/zip/%s.zip", flagBaseURL, a.repository),
 			Name:        a.appInfo.name,
 			Menu:        a.appInfo.menu,
 			Author:      a.appInfo.author,
@@ -355,3 +414,34 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) {
 	j := json.NewEncoder(w)
 	j.Encode(resp)
 }
+
+var (
+	reAppZipURL = regexp.MustCompile("^/api/apps/zip/([^/]+)/([^/]+).zip$")
+)
+
+func (s *server) handleAppZip(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+
+	matches := reAppZipURL.FindStringSubmatch(r.URL.Path)
+	if matches == nil {
+		http.NotFound(w, r)
+		return
+	}
+	orga := matches[1]
+	repo := matches[2]
+
+	apps, err := s.getApps(ctx)
+	if err != nil {
+		return
+	}
+
+	for _, app := range apps {
+		if app.repository == fmt.Sprintf("%s/%s", orga, repo) {
+			w.Header().Add("Content-Type", "application/zip")
+			w.Write(app.appInfo.zip)
+			return
+		}
+	}
+
+	http.NotFound(w, r)
+}
diff --git a/server_mirror.go b/server_mirror.go
index 9aff64c9e6ba97bbd285d4fe74caf5bfa0f4e675..e4f220394df1716086b7d0cf93a48be97faa64dc 100644
--- a/server_mirror.go
+++ b/server_mirror.go
@@ -8,6 +8,7 @@ import (
 	"io"
 	"log"
 	"net/http"
+	"regexp"
 	"strings"
 )
 
@@ -98,6 +99,10 @@ func (s *server) serveMirroredFile(w http.ResponseWriter, r *http.Request, rel *
 	http.NotFound(w, r)
 }
 
+var (
+	reMirrorURL = regexp.MustCompile("^/api/release/([^/]+)/([^/]+.bin)$")
+)
+
 func (s *server) handleReleaseMirror(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 
diff --git a/server_releases.go b/server_releases.go
index ffbef4ae14825ef9c76ba2e1b409a48cc6504996..7590e28769d6515bdd4bcae75bc219148da3c532 100644
--- a/server_releases.go
+++ b/server_releases.go
@@ -6,14 +6,9 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"regexp"
 	"time"
 )
 
-var (
-	reMirrorURL = regexp.MustCompile("^/api/release/([^/]+)/([^/]+.bin)$")
-)
-
 type GLAssetLink struct {
 	ID      int64  `json:"id"`
 	Name    string `json:"name"`