diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallationData.java
similarity index 53%
rename from src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java
rename to src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallationData.java
index 3783a36b912a0e5e3bc15e9197447ad3562b3826..4924c044307a8e177f8c15d5f89484b277799434 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallationData.java
@@ -11,6 +11,8 @@
  */
 package org.eclipsefoundation.git.eca.api.models;
 
+import javax.annotation.Nullable;
+
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 import com.google.auto.value.AutoValue;
@@ -22,8 +24,8 @@ import com.google.auto.value.AutoValue;
  *
  */
 @AutoValue
-@JsonDeserialize(builder = AutoValue_GithubApplicationInstallation.Builder.class)
-public abstract class GithubApplicationInstallation {
+@JsonDeserialize(builder = AutoValue_GithubApplicationInstallationData.Builder.class)
+public abstract class GithubApplicationInstallationData {
 
     public abstract int getId();
 
@@ -31,8 +33,10 @@ public abstract class GithubApplicationInstallation {
 
     public abstract String getTargetId();
 
+    public abstract Account getAccount();
+
     public static Builder builder() {
-        return new AutoValue_GithubApplicationInstallation.Builder();
+        return new AutoValue_GithubApplicationInstallationData.Builder();
     }
 
     @AutoValue.Builder
@@ -44,6 +48,31 @@ public abstract class GithubApplicationInstallation {
 
         public abstract Builder setTargetId(String targetId);
 
-        public abstract GithubApplicationInstallation build();
+        public abstract Builder setAccount(Account account);
+
+        public abstract GithubApplicationInstallationData build();
+    }
+
+    @AutoValue
+    @JsonDeserialize(builder = AutoValue_GithubApplicationInstallationData_Account.Builder.class)
+    public abstract static class Account {
+        public abstract int getId();
+
+        @Nullable
+        public abstract String getLogin();
+
+        public static Builder builder() {
+            return new AutoValue_GithubApplicationInstallationData_Account.Builder();
+        }
+
+        @AutoValue.Builder
+        @JsonPOJOBuilder(withPrefix = "set")
+        public abstract static class Builder {
+            public abstract Builder setId(int id);
+
+            public abstract Builder setLogin(@Nullable String login);
+
+            public abstract Account build();
+        }
     }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/WebhooksConfig.java b/src/main/java/org/eclipsefoundation/git/eca/config/WebhooksConfig.java
index 2d7f00dfa049ce7f29ebbbe51d4732ac78353f84..62cf9277d861af9a1743c9bfbd5e983d465ee637 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/config/WebhooksConfig.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/config/WebhooksConfig.java
@@ -25,5 +25,7 @@ public interface WebhooksConfig {
         String context();
 
         String serverTarget();
+        
+        int appId();
     }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/GithubApplicationInstallation.java b/src/main/java/org/eclipsefoundation/git/eca/dto/GithubApplicationInstallation.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c05c352e238bed370a2e6c9fb4535311d4123f9
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/GithubApplicationInstallation.java
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Author: Martin Lowe <martin.lowe@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.dto;
+
+import java.time.format.DateTimeParseException;
+import java.util.Date;
+import java.util.Objects;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.DtoTable;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
+
+/**
+ * Tracking for github installation data for GH ECA applications used for requesting data and updating commit statuses.
+ */
+@Table
+@Entity
+public class GithubApplicationInstallation extends BareNode {
+    public static final DtoTable TABLE = new DtoTable(GithubApplicationInstallation.class, "gai");
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private long id;
+    private int appId;
+    private int installationId;
+    private String name;
+    private Date lastUpdated;
+
+    @Override
+    public Long getId() {
+        return id;
+    }
+
+    /**
+     * @param id the id to set
+     */
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    /**
+     * @return the appId
+     */
+    public int getAppId() {
+        return appId;
+    }
+
+    /**
+     * @param appId the appId to set
+     */
+    public void setAppId(int appId) {
+        this.appId = appId;
+    }
+
+    /**
+     * @return the installationId
+     */
+    public int getInstallationId() {
+        return installationId;
+    }
+
+    /**
+     * @param installationId the installationId to set
+     */
+    public void setInstallationId(int installationId) {
+        this.installationId = installationId;
+    }
+
+    /**
+     * @return the name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name the name to set
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return the lastUpdated
+     */
+    public Date getLastUpdated() {
+        return lastUpdated;
+    }
+
+    /**
+     * @param lastUpdated the lastUpdated to set
+     */
+    public void setLastUpdated(Date lastUpdated) {
+        this.lastUpdated = lastUpdated;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + Objects.hash(appId, id, installationId, lastUpdated, name);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        GithubApplicationInstallation other = (GithubApplicationInstallation) obj;
+        return appId == other.appId && id == other.id && installationId == other.installationId
+                && Objects.equals(lastUpdated, other.lastUpdated) && Objects.equals(name, other.name);
+    }
+
+    @Singleton
+    public static class GithubApplicationInstallationFilter implements DtoFilter<GithubApplicationInstallation> {
+
+        @Inject
+        ParameterizedSQLStatementBuilder builder;
+
+        @Override
+        public ParameterizedSQLStatement getFilters(MultivaluedMap<String, String> params, boolean isRoot) {
+            ParameterizedSQLStatement statement = builder.build(TABLE);
+
+            String installationId = params.getFirst(GitEcaParameterNames.INSTALLATION_ID_RAW);
+            if (StringUtils.isNotBlank(installationId)) {
+                statement
+                        .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".installationId = ?",
+                                new Object[] { Integer.parseInt(installationId) }));
+            }
+            String appId = params.getFirst(GitEcaParameterNames.APPLICATION_ID_RAW);
+            if (StringUtils.isNumeric(appId)) {
+                statement
+                        .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".appId = ?",
+                                new Object[] { Integer.parseInt(appId) }));
+            }
+            String lastUpdated = params.getFirst(GitEcaParameterNames.LAST_UPDATED_BEFORE_RAW);
+            if (StringUtils.isNotBlank(lastUpdated)) {
+                try {
+                    Date dt = DateTimeHelper.toRFC3339(lastUpdated);
+                    statement.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".lastUpdated < ?", new Object[] { dt }));
+                } catch (DateTimeParseException e) {
+                    // do not do anything for badly formatted args
+                }
+            }
+            return statement;
+        }
+
+        @Override
+        public Class<GithubApplicationInstallation> getType() {
+            return GithubApplicationInstallation.class;
+        }
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/GithubValidationHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/GithubValidationHelper.java
index f5ec3697211aed0b29a79da6e47d7a3ec328b15a..6fa08c4c4554e6a0809fd6c37a9388f7d5b079aa 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/helper/GithubValidationHelper.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/helper/GithubValidationHelper.java
@@ -110,7 +110,7 @@ public class GithubValidationHelper {
         // use logging helper to sanitize newlines as they aren't needed here
         String fullRepoName = LoggingHelper.format(org + '/' + repoName);
         // get the installation ID for the given repo if it exists, and if the PR noted exists
-        String installationId = ghAppService.getInstallationForRepo(fullRepoName);
+        String installationId = ghAppService.getInstallationForRepo(org, repoName);
         Optional<PullRequest> prResponse = ghAppService.getPullRequest(installationId, fullRepoName, prNo);
         if (StringUtils.isBlank(installationId)) {
             throw new BadRequestException("Could not find an ECA app installation for repo name: " + fullRepoName);
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
index 70a657c1a48e4b7e87957692b2477222f53f2565..35d4fc2464cf23190069db5d18806fd2d89a8323 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
@@ -37,6 +37,8 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public static final String UNTIL_RAW = "until";
     public static final String REPOSITORY_FULL_NAME_RAW = "repo_full_name";
     public static final String INSTALLATION_ID_RAW = "installation_id";
+    public static final String APPLICATION_ID_RAW = "application_id";
+    public static final String LAST_UPDATED_BEFORE_RAW = "last_updated_before";
     public static final String PULL_REQUEST_NUMBER_RAW = "pull_request_number";
     public static final String USER_MAIL_RAW = "user_mail";
     public static final String NEEDS_REVALIDATION_RAW = "needs_revalidation";
@@ -57,6 +59,8 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public static final UrlParameter UNTIL = new UrlParameter(UNTIL_RAW);
     public static final UrlParameter REPOSITORY_FULL_NAME = new UrlParameter(REPOSITORY_FULL_NAME_RAW);
     public static final UrlParameter INSTALLATION_ID = new UrlParameter(INSTALLATION_ID_RAW);
+    public static final UrlParameter APPLICATION_ID = new UrlParameter(APPLICATION_ID_RAW);
+    public static final UrlParameter LAST_UPDATED_BEFORE = new UrlParameter(LAST_UPDATED_BEFORE_RAW);
     public static final UrlParameter PULL_REQUEST_NUMBER = new UrlParameter(PULL_REQUEST_NUMBER_RAW);
     public static final UrlParameter USER_MAIL = new UrlParameter(USER_MAIL_RAW);
     public static final UrlParameter NEEDS_REVALIDATION = new UrlParameter(NEEDS_REVALIDATION_RAW);
@@ -65,8 +69,8 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public List<UrlParameter> getParameters() {
         return Arrays
                 .asList(COMMIT_ID, SHA, SHAS, PROJECT_ID, PROJECT_IDS, NOT_IN_PROJECT_IDS, REPO_URL, FINGERPRINT, USER_ID, PROJECT_PATH,
-                        PARENT_PROJECT, STATUS_ACTIVE, STATUS_DELETED, SINCE, UNTIL, REPOSITORY_FULL_NAME, INSTALLATION_ID,
-                        PULL_REQUEST_NUMBER, USER_MAIL, NEEDS_REVALIDATION);
+                        PARENT_PROJECT, STATUS_ACTIVE, STATUS_DELETED, SINCE, UNTIL, REPOSITORY_FULL_NAME, INSTALLATION_ID, APPLICATION_ID,
+                        LAST_UPDATED_BEFORE, PULL_REQUEST_NUMBER, USER_MAIL, NEEDS_REVALIDATION);
     }
 
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
index 0db2c74dd31461b8f4a43296e1ae9a4dac87431a..630ae1f1063cf1ba305b9b7c95eb077225559362 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
@@ -122,7 +122,7 @@ public class StatusResource extends GithubAdjacentResource {
             @PathParam("prNo") Integer prNo) {
         String repoFullName = org + '/' + repoName;
         // check that the passed repo has a valid installation
-        String installationId = ghAppService.getInstallationForRepo(repoFullName);
+        String installationId = ghAppService.getInstallationForRepo(org, repoName);
         if (StringUtils.isBlank(installationId)) {
             throw new BadRequestException("Repo " + repoFullName + " requested, but does not have visible installation, returning");
         }
@@ -158,7 +158,7 @@ public class StatusResource extends GithubAdjacentResource {
                         .data("fullRepoName", repoFullName)
                         .data("project", ps.isEmpty() ? null : ps.get(0))
                         .data("repoUrl", repoUrl)
-                        .data("installationId", ghAppService.getInstallationForRepo(repoFullName))
+                        .data("installationId", installationId)
                         .render())
                 .build();
     }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java
index 4b6cafab702e0c31e24762399649287b11c3e3a5..ae0a120f4c6ad886903f6c1d8183d974a65ec25c 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java
@@ -11,11 +11,11 @@
  */
 package org.eclipsefoundation.git.eca.service;
 
+import java.util.List;
 import java.util.Optional;
 
-import javax.ws.rs.core.MultivaluedMap;
-
 import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
+import org.eclipsefoundation.git.eca.dto.GithubApplicationInstallation;
 
 /**
  * Service for interacting with the Github API with caching for performance.
@@ -26,19 +26,20 @@ import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest
 public interface GithubApplicationService {
 
     /**
-     * Retrieve safe map of all installations that are managed by this instance of the Eclispe ECA app.
+     * Retrieve safe list of all installations that are managed by this instance of the Eclispe ECA app.
      * 
-     * @return map containing installation IDs mapped to the list of repos they support for the current app
+     * @return list containing installation records for the current application
      */
-    MultivaluedMap<String, String> getManagedInstallations();
+    List<GithubApplicationInstallation> getManagedInstallations();
 
     /**
-     * Retrieves the installation ID for the ECA app on the given repo if it exists.
+     * Retrieves the installation ID for the ECA app on the given org or repo if it exists.
      * 
-     * @param repoFullName the full repo name to retrieve an installation ID for. E.g. eclipse/jetty
+     * @param org the name of organization to retrieve an installation ID for
+     * @param repo the name of repo to retrieve an installation ID for
      * @return the numeric installation ID if it exists, or null
      */
-    String getInstallationForRepo(String repoFullName);
+    String getInstallationForRepo(String org, String repo);
 
     /**
      * Retrieves a pull request given the repo, pull request, and associated installation to action the fetch.
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java
index d1f04c3356fc1c5ca566720395a6b17ae1bcb1cf..ca5cd9e74fe1f77d5f37ec1cc312b614519d0c12 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java
@@ -11,8 +11,10 @@
  */
 package org.eclipsefoundation.git.eca.service.impl;
 
+import java.net.URI;
 import java.time.Duration;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
@@ -26,14 +28,20 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.context.ManagedExecutor;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.exception.ApplicationException;
+import org.eclipsefoundation.core.model.FlatRequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.service.APIMiddleware;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.GithubAPI;
-import org.eclipsefoundation.git.eca.api.models.GithubApplicationInstallation;
-import org.eclipsefoundation.git.eca.api.models.GithubInstallationRepositoriesResponse;
 import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
+import org.eclipsefoundation.git.eca.config.WebhooksConfig;
+import org.eclipsefoundation.git.eca.dto.GithubApplicationInstallation;
 import org.eclipsefoundation.git.eca.helper.JwtHelper;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
 import org.eclipsefoundation.git.eca.service.GithubApplicationService;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.service.FilterService;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -58,21 +66,27 @@ public class DefaultGithubApplicationService implements GithubApplicationService
 
     @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28")
     String apiVersion;
+    @Inject
+    WebhooksConfig config;
 
     @RestClient
     GithubAPI gh;
 
     @Inject
-    JwtHelper jwt;
+    PersistenceDao dao;
+    @Inject
+    FilterService filters;
     @Inject
     CachingService cache;
     @Inject
     APIMiddleware middle;
 
+    @Inject
+    JwtHelper jwt;
     @Inject
     ManagedExecutor exec;
 
-    private AsyncLoadingCache<String, MultivaluedMap<String, String>> installationRepositoriesCache;
+    private AsyncLoadingCache<String, List<GithubApplicationInstallation>> installationRepositoriesCache;
 
     @PostConstruct
     void init() {
@@ -87,17 +101,18 @@ public class DefaultGithubApplicationService implements GithubApplicationService
     }
 
     @Override
-    public MultivaluedMap<String, String> getManagedInstallations() {
-        // create a deep copy of the map to protect from modifications and return it
-        MultivaluedMap<String, String> mapClone = new MultivaluedMapImpl<>();
-        getAllInstallRepos().entrySet().stream().forEach(e -> mapClone.put(e.getKey(), new ArrayList<>(e.getValue())));
-        return mapClone;
+    public List<GithubApplicationInstallation> getManagedInstallations() {
+        return new ArrayList<>(getAllInstallRepos());
     }
 
     @Override
-    public String getInstallationForRepo(String repoFullName) {
-        MultivaluedMap<String, String> map = getAllInstallRepos();
-        return map.keySet().stream().filter(k -> map.get(k).contains(repoFullName)).findFirst().orElse(null);
+    public String getInstallationForRepo(String org, String repo) {
+        return getAllInstallRepos()
+                .stream()
+                .filter(install -> install.getName().equalsIgnoreCase(org))
+                .map(install -> Integer.toString(install.getInstallationId()))
+                .findFirst()
+                .orElse(null);
     }
 
     @Override
@@ -107,7 +122,7 @@ public class DefaultGithubApplicationService implements GithubApplicationService
                         () -> gh.getPullRequest(jwt.getGhBearerString(installationId), apiVersion, repoFullName, pullRequest));
     }
 
-    private MultivaluedMap<String, String> getAllInstallRepos() {
+    private List<GithubApplicationInstallation> getAllInstallRepos() {
         try {
             return this.installationRepositoriesCache.get("all").get(INSTALL_REPO_FETCH_MAX_TIMEOUT, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
@@ -118,42 +133,22 @@ public class DefaultGithubApplicationService implements GithubApplicationService
             throw new ApplicationException(
                     "Thread interrupted while building repository cache, no entries will be available for current call", e);
         }
-        return new MultivaluedMapImpl<>();
+        return Collections.emptyList();
     }
 
     /**
-     * Retrieves a fresh copy of installation repositories, mapped by installation ID to associated full repo names.
+     * Retrieves a fresh copy of tracked installations, which have the associated app, the installation ID, and the name of the namespace associated with it.
      * 
      * @return a multivalued map relating installation IDs to associated full repo names.
      */
-    private MultivaluedMap<String, String> loadInstallationRepositories() {
-        // create map early for potential empty returns
-        MultivaluedMapImpl<String, String> out = new MultivaluedMapImpl<>();
-        // get JWT bearer and then get all associated installations of current app
-        String auth = "Bearer " + jwt.generateJwt();
-        List<GithubApplicationInstallation> installations = middle
-                .getAll(i -> gh.getInstallations(i, auth), GithubApplicationInstallation.class);
-        // check that there are installations
-        if (installations.isEmpty()) {
-            LOGGER.warn("Did not find any installations for the currently configured Github application");
-            return out;
-        }
-        // trace log the installations for more context
-        LOGGER.trace("Found the following installations: {}", installations);
-
-        // from installations, get the assoc. repos and grab their full repo name and collect them
-        installations
-                .stream()
-                .forEach(installation -> middle
-                        .getAll(i -> gh.getInstallationRepositories(i, jwt.getGhBearerString(Integer.toString(installation.getId()))),
-                                GithubInstallationRepositoriesResponse.class)
-                        .stream()
-                        .forEach(installRepo -> installRepo
-                                .getRepositories()
-                                .stream()
-                                .forEach(r -> out.add(Integer.toString(installation.getId()), r.getFullName()))));
-        LOGGER.trace("Final results for generating installation mapping: {}", out);
-        return out;
+    private List<GithubApplicationInstallation> loadInstallationRepositories() {
+        // once records are prepared, persist them back to the database with updates where necessary as a batch
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("https://api.eclipse.org/git/webhooks/github/installations"));
+
+        // build query to do fetch of records for currently active application
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.add(GitEcaParameterNames.APPLICATION_ID_RAW, Integer.toString(config.github().appId()));
+        return dao.get(new RDBMSQuery<>(wrap, filters.get(GithubApplicationInstallation.class), params));
     }
 
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubInstallationUpdateTask.java b/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubInstallationUpdateTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..30c431fe16d9333e7dc436c6863952a1f9fb5eff
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/tasks/GithubInstallationUpdateTask.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Author: Martin Lowe <martin.lowe@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.tasks;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.control.ActivateRequestContext;
+import javax.enterprise.inject.Instance;
+import javax.inject.Inject;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.maven.shared.utils.StringUtils;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.core.model.FlatRequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.APIMiddleware;
+import org.eclipsefoundation.git.eca.api.GithubAPI;
+import org.eclipsefoundation.git.eca.api.models.GithubApplicationInstallationData;
+import org.eclipsefoundation.git.eca.config.WebhooksConfig;
+import org.eclipsefoundation.git.eca.dto.GithubApplicationInstallation;
+import org.eclipsefoundation.git.eca.helper.JwtHelper;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.service.FilterService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.quarkus.scheduler.Scheduled;
+
+/**
+ * Using the configured database, Github installation records are tracked for all installations available for the
+ * currently configured GH application. This is used over an in-memory cache as the calls to build the cache are long
+ * running and have been known to fail sporadically.
+ */
+@ApplicationScoped
+public class GithubInstallationUpdateTask {
+    private static final Logger LOGGER = LoggerFactory.getLogger(GithubInstallationUpdateTask.class);
+
+    @ConfigProperty(name = "eclipse.git-eca.tasks.gh-installation.enabled", defaultValue = "true")
+    Instance<Boolean> isEnabled;
+    @Inject
+    WebhooksConfig config;
+
+    @RestClient
+    GithubAPI gh;
+
+    @Inject
+    JwtHelper jwt;
+    @Inject
+    APIMiddleware middle;
+    @Inject
+    PersistenceDao dao;
+    @Inject
+    FilterService filters;
+
+    @PostConstruct
+    void init() {
+        // indicate to log whether enabled to reduce log spam
+        LOGGER.info("Github installation DB cache task is{} enabled.", Boolean.TRUE.equals(isEnabled.get()) ? "" : " not");
+    }
+
+    /**
+     * Every 1h, this method will query Github for installations for the currently configured application. These
+     * installations will be translated into database entries for a persistent cache to be loaded on request.
+     */
+    @Scheduled(every = "1h")
+    @ActivateRequestContext
+    public void revalidate() {
+        // if not enabled, don't process any potentially OOD records
+        if (!Boolean.TRUE.equals(isEnabled.get())) {
+            return;
+        }
+
+        // get the installations for the currently configured app
+        List<GithubApplicationInstallationData> installations = middle
+                .getAll(i -> gh.getInstallations(i, "Bearer " + jwt.generateJwt()), GithubApplicationInstallationData.class);
+        // check that there are installations
+        if (installations.isEmpty()) {
+            LOGGER.warn("Did not find any installations for the currently configured Github application");
+            return;
+        }
+        // trace log the installations for more context
+        LOGGER.debug("Found {} installations to cache", installations.size());
+
+        // create a common timestamp for easier lookups of stale entries
+        Date startingTimestamp = new Date();
+        // from installations, build records and start the processing for each entry
+        List<GithubApplicationInstallation> installationRecords = installations
+                .stream()
+                .map(this::processInstallation)
+                .collect(Collectors.toList());
+
+        // once records are prepared, persist them back to the database with updates where necessary as a batch
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("https://api.eclipse.org/git/webhooks/github/installations"));
+        List<GithubApplicationInstallation> repoRecords = dao
+                .add(new RDBMSQuery<>(wrap, filters.get(GithubApplicationInstallation.class)), installationRecords);
+        if (repoRecords.size() != installationRecords.size()) {
+            LOGGER.warn("Background update to installation records had a size mismatch, cleaning will be skipped for this run");
+            return;
+        }
+
+        // build query to do cleanup of stale records
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.add(GitEcaParameterNames.APPLICATION_ID_RAW, Integer.toString(config.github().appId()));
+        params.add(GitEcaParameterNames.LAST_UPDATED_BEFORE_RAW, DateTimeHelper.toRFC3339(startingTimestamp));
+
+        // run the delete call, removing stale entries
+        dao.delete(new RDBMSQuery<>(wrap, filters.get(GithubApplicationInstallation.class), params));
+    }
+
+    /**
+     * Converts the raw installation data from Github into a short record to be persisted to database as a form of
+     * persistent caching. Checks database for existing record, and returns record with a touched date for existing entries.
+     * 
+     * @param ghInstallation raw Github installation record for current application
+     * @return the new or updated installation record to be persisted to the database.
+     */
+    private GithubApplicationInstallation processInstallation(GithubApplicationInstallationData ghInstallation) {
+        RequestWrapper wrap = new FlatRequestWrapper(URI.create("https://api.eclipse.org/git/webhooks/github/installations"));
+        // build the lookup query for the current installation record
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.add(GitEcaParameterNames.APPLICATION_ID_RAW, Integer.toString(config.github().appId()));
+        params.add(GitEcaParameterNames.INSTALLATION_ID_RAW, Integer.toString(ghInstallation.getId()));
+
+        // lookup existing records in the database
+        List<GithubApplicationInstallation> existingRecords = dao
+                .get(new RDBMSQuery<>(wrap, filters.get(GithubApplicationInstallation.class), params));
+
+        // check for existing entry, creating if missing
+        GithubApplicationInstallation installation;
+        if (existingRecords == null || existingRecords.isEmpty()) {
+            installation = new GithubApplicationInstallation();
+            installation.setAppId(config.github().appId());
+            installation.setInstallationId(ghInstallation.getId());
+        } else {
+            installation = existingRecords.get(0);
+        }
+        // update the basic stats to handle renames, and update last updated time
+        // login is technically nullable, so this might be missing. This is best we can do, as we can't look up by id
+        installation
+                .setName(StringUtils.isNotBlank(ghInstallation.getAccount().getLogin()) ? ghInstallation.getAccount().getLogin()
+                        : "UNKNOWN");
+        installation.setLastUpdated(new Date());
+        return installation;
+
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f113468ea24cac9a9228775226d298540d23034c..08e8aee3d41aa3a46b6551f2a361240481a8e5ed 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -40,13 +40,14 @@ quarkus.cache.caffeine."record".expire-after-write=${quarkus.cache.caffeine."def
 quarkus.cache.caffeine."accesstoken".initial-capacity=1000
 quarkus.cache.caffeine."accesstoken".expire-after-write=119S
 
-## JWT Placeholders/defaults
-smallrye.jwt.new-token.lifespan=120
-smallrye.jwt.new-token.issuer=262450
-
 ## Webhook configs
 eclipse.webhooks.github.context=eclipsefdn/eca
 eclipse.webhooks.github.server-target=https://api.eclipse.org
+eclipse.webhooks.github.app-id=262450
+
+## JWT Placeholders/defaults
+smallrye.jwt.new-token.lifespan=120
+smallrye.jwt.new-token.issuer=${eclipse.webhooks.github.app-id}
 
 ## Git-eca mail configs
 eclipse.git-eca.mail-validation.allow-list=noreply@github.com,49699333+dependabot[bot]@users.noreply.github.com,bot@stepsecurity.io
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 4b362d9386195a064dfb14940077dce551c3236d..5599303b22ae4d2771ce35c8a1a2beb4a6109cd9 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -35,6 +35,7 @@ eclipse.git-eca.mail.allow-list=noreply@github.com
 ## Reports configs
 eclipse.git-eca.reports.access-key=samplekey
 eclipse.git-eca.tasks.gh-revalidation.enabled=false
+eclipse.git-eca.tasks.gh-installation.enabled=false
 
 ## Misc
 eclipse.gitlab.access-token=token_val