diff --git a/main.go b/main.go
index f6001029682b5d9eeb43060492c7410ea05110a2..9b9e61f805d9f33369b487b73cb104c1baecf168 100644
--- a/main.go
+++ b/main.go
@@ -32,6 +32,7 @@ func main() {
 	http.HandleFunc("/api/releases.json", s.handleReleases)
 	http.HandleFunc("/api/release/", s.handleReleaseMirror)
 	http.HandleFunc("/api/apps/zip/", s.handleAppZip)
+	http.HandleFunc("/api/apps/tar/", s.handleAppTargz)
 	log.Printf("Listening on %s...", flagListen)
 	http.ListenAndServe(flagListen, nil)
 }
diff --git a/server.go b/server.go
index 6eb3daca0f6baa296443ab01a4677c38b32e4808..85cfd4931d1bce090ab3afbab38d79eef9715eb0 100644
--- a/server.go
+++ b/server.go
@@ -35,7 +35,7 @@ func (s *server) run(ctx context.Context) {
 		log.Fatalf("Initial app fetch failed: %v", err)
 	}
 
-	t := time.NewTicker(60 * time.Second)
+	t := time.NewTicker(5 * 60 * time.Second)
 	for {
 		select {
 		case r := <-s.reqC:
diff --git a/server_apps.go b/server_apps.go
index d39377d5cedaa34b85ab99fca070739cd0727df8..b05800a6d4913971853e1be8651d1a6e74882649 100644
--- a/server_apps.go
+++ b/server_apps.go
@@ -1,8 +1,10 @@
 package main
 
 import (
+	"archive/tar"
 	"archive/zip"
 	"bytes"
+	"compress/gzip"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -40,6 +42,7 @@ type appInfo struct {
 
 	commitObj *object.Commit
 	zip       []byte
+	targz     []byte
 }
 
 type GLProject struct {
@@ -128,6 +131,65 @@ func (s *server) zipApp(ctx context.Context, name, pathInRepo, repo string, obj
 	return buf.Bytes(), nil
 }
 
+func (s *server) targzApp(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)
+	gz := gzip.NewWriter(buf)
+	t := tar.NewWriter(gz)
+	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)
+		err = t.WriteHeader(&tar.Header{
+			Name:     outPath,
+			Typeflag: tar.TypeReg,
+			Size:     f.Size,
+		})
+		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(t, rdr)
+		rdr.Close()
+		if err != nil {
+			return nil, fmt.Errorf("when copying: %w", err)
+		}
+	}
+	if err := t.Close(); err != nil {
+		return nil, fmt.Errorf("when closing tar: %w", err)
+	}
+	if err := gz.Close(); err != nil {
+		return nil, fmt.Errorf("when closing gz: %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)
@@ -268,6 +330,11 @@ func (s *server) getAppInfo(ctx context.Context, pathInRepo, repo string) (*appI
 		return nil, fmt.Errorf("zipping failed: %w", err)
 	}
 	app.zip = zbytes
+	tbytes, err := s.targzApp(ctx, app.name, pathInRepo, repo, app.commitObj)
+	if err != nil {
+		return nil, fmt.Errorf("targzing failed: %w", err)
+	}
+	app.targz = tbytes
 	return app, nil
 }
 
@@ -373,15 +440,16 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) {
 	}
 
 	type app struct {
-		RepoURL     string `json:"repoUrl"`
-		Commit      string `json:"commit"`
-		DownloadURL string `json:"downloadUrl"`
-		Name        string `json:"name"`
-		Menu        string `json:"menu"`
-		Author      string `json:"author"`
-		Description string `json:"description"`
-		Version     int    `json:"version"`
-		Stars       int    `json:"stars"`
+		RepoURL        string `json:"repoUrl"`
+		Commit         string `json:"commit"`
+		DownloadURL    string `json:"downloadUrl"`
+		TarDownloadURL string `json:"tarDownloadUrl"`
+		Name           string `json:"name"`
+		Menu           string `json:"menu"`
+		Author         string `json:"author"`
+		Description    string `json:"description"`
+		Version        int    `json:"version"`
+		Stars          int    `json:"stars"`
 	}
 
 	type res struct {
@@ -398,15 +466,16 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) {
 		}
 		// Guarenteed to be exactly 2 parts because of the regex
 		resp.Apps = append(resp.Apps, app{
-			RepoURL:     "https://git.flow3r.garden/" + a.repository,
-			Commit:      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,
-			Description: a.appInfo.description,
-			Version:     a.appInfo.version,
-			Stars:       a.appInfo.stars,
+			RepoURL:        "https://git.flow3r.garden/" + a.repository,
+			Commit:         a.appInfo.commit,
+			DownloadURL:    fmt.Sprintf("%sapps/zip/%s.zip", flagBaseURL, a.repository),
+			TarDownloadURL: fmt.Sprintf("%sapps/tar/%s.tar.gz", flagBaseURL, a.repository),
+			Name:           a.appInfo.name,
+			Menu:           a.appInfo.menu,
+			Author:         a.appInfo.author,
+			Description:    a.appInfo.description,
+			Version:        a.appInfo.version,
+			Stars:          a.appInfo.stars,
 		})
 	}
 	w.Header().Add("Access-Control-Allow-Origin", "*")
@@ -416,7 +485,8 @@ func (s *server) handleApps(w http.ResponseWriter, r *http.Request) {
 }
 
 var (
-	reAppZipURL = regexp.MustCompile("^/api/apps/zip/([^/]+)/([^/]+).zip$")
+	reAppZipURL   = regexp.MustCompile(`^/api/apps/zip/([^/]+)/([^/]+)\.zip$`)
+	reAppTargzURL = regexp.MustCompile(`^/api/apps/tar/([^/]+)/([^/]+)\.tar\.gz$`)
 )
 
 func (s *server) handleAppZip(w http.ResponseWriter, r *http.Request) {
@@ -445,3 +515,30 @@ func (s *server) handleAppZip(w http.ResponseWriter, r *http.Request) {
 
 	http.NotFound(w, r)
 }
+
+func (s *server) handleAppTargz(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+
+	matches := reAppTargzURL.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/x-gzip")
+			w.Write(app.appInfo.targz)
+			return
+		}
+	}
+
+	http.NotFound(w, r)
+}