diff --git a/collector/extensions/index.js b/collector/extensions/index.js
new file mode 100644
index 000000000..7b131b646
--- /dev/null
+++ b/collector/extensions/index.js
@@ -0,0 +1,52 @@
+const { reqBody } = require("../utils/http");
+
+function extensions(app) {
+  if (!app) return;
+
+  app.post("/ext/github-repo", async function (request, response) {
+    try {
+      const loadGithubRepo = require("../utils/extensions/GithubRepo");
+      const { success, reason, data } = await loadGithubRepo(reqBody(request));
+      response.status(200).json({
+        success,
+        reason,
+        data
+      });
+    } catch (e) {
+      console.error(e);
+      response.status(200).json({
+        success: false,
+        reason: e.message || "A processing error occurred.",
+        data: {},
+      });
+    }
+    return;
+  });
+
+  // gets all branches for a specific repo
+  app.post("/ext/github-repo/branches", async function (request, response) {
+    try {
+      const GithubRepoLoader = require("../utils/extensions/GithubRepo/RepoLoader");
+      const allBranches = await (new GithubRepoLoader(reqBody(request))).getRepoBranches()
+      response.status(200).json({
+        success: true,
+        reason: null,
+        data: {
+          branches: allBranches
+        }
+      });
+    } catch (e) {
+      console.error(e);
+      response.status(400).json({
+        success: false,
+        reason: e.message,
+        data: {
+          branches: []
+        }
+      });
+    }
+    return;
+  });
+}
+
+module.exports = extensions;
diff --git a/collector/index.js b/collector/index.js
index 4cbca6c21..5070ae72f 100644
--- a/collector/index.js
+++ b/collector/index.js
@@ -11,6 +11,7 @@ const { reqBody } = require("./utils/http");
 const { processSingleFile } = require("./processSingleFile");
 const { processLink } = require("./processLink");
 const { wipeCollectorStorage } = require("./utils/files");
+const extensions = require("./extensions");
 const app = express();
 
 app.use(cors({ origin: true }));
@@ -57,6 +58,8 @@ app.post("/process-link", async function (request, response) {
   return;
 });
 
+extensions(app);
+
 app.get("/accepts", function (_, response) {
   response.status(200).json(ACCEPTED_MIMES);
 });
diff --git a/collector/package.json b/collector/package.json
index 180a20888..fb9bed67a 100644
--- a/collector/package.json
+++ b/collector/package.json
@@ -24,6 +24,7 @@
     "express": "^4.18.2",
     "extract-zip": "^2.0.1",
     "fluent-ffmpeg": "^2.1.2",
+    "ignore": "^5.3.0",
     "js-tiktoken": "^1.0.8",
     "langchain": "0.0.201",
     "mammoth": "^1.6.0",
@@ -35,6 +36,7 @@
     "pdf-parse": "^1.1.1",
     "puppeteer": "^21.6.1",
     "slugify": "^1.6.6",
+    "url-pattern": "^1.0.3",
     "uuid": "^9.0.0",
     "wavefile": "^11.0.0"
   },
@@ -42,4 +44,4 @@
     "nodemon": "^2.0.22",
     "prettier": "^2.4.1"
   }
-}
\ No newline at end of file
+}
diff --git a/collector/utils/extensions/GithubRepo/RepoLoader/index.js b/collector/utils/extensions/GithubRepo/RepoLoader/index.js
new file mode 100644
index 000000000..7f1c1c057
--- /dev/null
+++ b/collector/utils/extensions/GithubRepo/RepoLoader/index.js
@@ -0,0 +1,149 @@
+class RepoLoader {
+  constructor(args = {}) {
+    this.ready = false;
+    this.repo = args?.repo;
+    this.branch = args?.branch;
+    this.accessToken = args?.accessToken || null;
+    this.ignorePaths = args?.ignorePaths || [];
+
+    this.author = null;
+    this.project = null;
+    this.branches = [];
+  }
+
+  #validGithubUrl() {
+    const UrlPattern = require("url-pattern");
+    const pattern = new UrlPattern("https\\://github.com/(:author)/(:project)");
+    const match = pattern.match(this.repo);
+    if (!match) return false;
+
+    this.author = match.author;
+    this.project = match.project;
+    return true;
+  }
+
+  // Ensure the branch provided actually exists
+  // and if it does not or has not been set auto-assign to primary branch.
+  async #validBranch() {
+    await this.getRepoBranches();
+    if (!!this.branch && this.branches.includes(this.branch)) return;
+
+    console.log(
+      "[Github Loader]: Branch not set! Auto-assigning to a default branch."
+    );
+    this.branch = this.branches.includes("main") ? "main" : "master";
+    console.log(`[Github Loader]: Branch auto-assigned to ${this.branch}.`);
+    return;
+  }
+
+  async #validateAccessToken() {
+    if (!this.accessToken) return;
+    const valid = await fetch("https://api.github.com/octocat", {
+      method: "GET",
+      headers: {
+        Authorization: `Bearer ${this.accessToken}`,
+        "X-GitHub-Api-Version": "2022-11-28",
+      },
+    })
+      .then((res) => {
+        if (!res.ok) throw new Error(res.statusText);
+        return res.ok;
+      })
+      .catch((e) => {
+        console.error(
+          "Invalid Github Access Token provided! Access token will not be used",
+          e.message
+        );
+        return false;
+      });
+
+    if (!valid) this.accessToken = null;
+    return;
+  }
+
+  async init() {
+    if (!this.#validGithubUrl()) return;
+    await this.#validBranch();
+    await this.#validateAccessToken();
+    this.ready = true;
+    return this;
+  }
+
+  async recursiveLoader() {
+    if (!this.ready) throw new Error("[Github Loader]: not in ready state!");
+    const {
+      GithubRepoLoader: LCGithubLoader,
+    } = require("langchain/document_loaders/web/github");
+
+    if (this.accessToken)
+      console.log(
+        `[Github Loader]: Access token set! Recursive loading enabled!`
+      );
+
+    const loader = new LCGithubLoader(this.repo, {
+      accessToken: this.accessToken,
+      branch: this.branch,
+      recursive: !!this.accessToken, // Recursive will hit rate limits.
+      maxConcurrency: 5,
+      unknown: "ignore",
+      ignorePaths: this.ignorePaths,
+    });
+
+    const docs = [];
+    for await (const doc of loader.loadAsStream()) docs.push(doc);
+    return docs;
+  }
+
+  // Sort branches to always show either main or master at the top of the result.
+  #branchPrefSort(branches = []) {
+    const preferredSort = ["main", "master"];
+    return branches.reduce((acc, branch) => {
+      if (preferredSort.includes(branch)) return [branch, ...acc];
+      return [...acc, branch];
+    }, []);
+  }
+
+  // Get all branches for a given repo.
+  async getRepoBranches() {
+    if (!this.#validGithubUrl() || !this.author || !this.project) return [];
+    await this.#validateAccessToken(); // Ensure API access token is valid for pre-flight
+
+    let page = 0;
+    let polling = true;
+    const branches = [];
+
+    while (polling) {
+      console.log(`Fetching page ${page} of branches for ${this.project}`);
+      await fetch(
+        `https://api.github.com/repos/${this.author}/${this.project}/branches?per_page=100&page=${page}`,
+        {
+          method: "GET",
+          headers: {
+            ...(this.accessToken
+              ? { Authorization: `Bearer ${this.accessToken}` }
+              : {}),
+            "X-GitHub-Api-Version": "2022-11-28",
+          },
+        }
+      )
+        .then((res) => {
+          if (res.ok) return res.json();
+          throw new Error(`Invalid request to Github API: ${res.statusText}`);
+        })
+        .then((branchObjects) => {
+          polling = branchObjects.length > 0;
+          branches.push(branchObjects.map((branch) => branch.name));
+          page++;
+        })
+        .catch((err) => {
+          polling = false;
+          console.log(`RepoLoader.branches`, err);
+        });
+    }
+
+    this.branches = [...new Set(branches.flat())];
+    return this.#branchPrefSort(this.branches);
+  }
+}
+
+module.exports = RepoLoader;
diff --git a/collector/utils/extensions/GithubRepo/index.js b/collector/utils/extensions/GithubRepo/index.js
new file mode 100644
index 000000000..d459e6357
--- /dev/null
+++ b/collector/utils/extensions/GithubRepo/index.js
@@ -0,0 +1,78 @@
+const RepoLoader = require("./RepoLoader");
+const fs = require("fs");
+const path = require("path");
+const { default: slugify } = require("slugify");
+const { v4 } = require("uuid");
+const { writeToServerDocuments } = require("../../files");
+const { tokenizeString } = require("../../tokenizer");
+
+async function loadGithubRepo(args) {
+  const repo = new RepoLoader(args);
+  await repo.init();
+
+  if (!repo.ready)
+    return {
+      success: false,
+      reason: "Could not prepare Github repo for loading! Check URL",
+    };
+
+  console.log(
+    `-- Working Github ${repo.author}/${repo.project}:${repo.branch} --`
+  );
+  const docs = await repo.recursiveLoader();
+  if (!docs.length) {
+    return {
+      success: false,
+      reason: "No files were found for those settings.",
+    };
+  }
+
+  console.log(`[Github Loader]: Found ${docs.length} source files. Saving...`);
+  const outFolder = slugify(
+    `${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}`
+  ).toLowerCase();
+  const outFolderPath = path.resolve(
+    __dirname,
+    `../../../../server/storage/documents/${outFolder}`
+  );
+  fs.mkdirSync(outFolderPath);
+
+  for (const doc of docs) {
+    if (!doc.pageContent) continue;
+    const data = {
+      id: v4(),
+      url: "github://" + doc.metadata.source,
+      title: doc.metadata.source,
+      docAuthor: repo.author,
+      description: "No description found.",
+      docSource: repo.repo,
+      chunkSource: doc.metadata.source,
+      published: new Date().toLocaleString(),
+      wordCount: doc.pageContent.split(" ").length,
+      pageContent: doc.pageContent,
+      token_count_estimate: tokenizeString(doc.pageContent).length,
+    };
+    console.log(
+      `[Github Loader]: Saving ${doc.metadata.source} to ${outFolder}`
+    );
+    writeToServerDocuments(
+      data,
+      `${slugify(doc.metadata.source)}-${data.id}`,
+      outFolderPath
+    );
+  }
+
+  return {
+    success: true,
+    reason: null,
+    data: {
+      author: repo.author,
+      repo: repo.project,
+      branch: repo.branch,
+      files: docs.length,
+      destination: outFolder,
+    },
+  };
+}
+
+module.exports = loadGithubRepo;
diff --git a/collector/yarn.lock b/collector/yarn.lock
index c6f442a80..28c610926 100644
--- a/collector/yarn.lock
+++ b/collector/yarn.lock
@@ -1530,6 +1530,11 @@ ignore-by-default@^1.0.1:
   resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
   integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
 
+ignore@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
+  integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
+
 immediate@~3.0.5:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
@@ -3127,6 +3132,11 @@ unpipe@1.0.0, unpipe@~1.0.0:
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
   integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
 
+url-pattern@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1"
+  integrity sha512-uQcEj/2puA4aq1R3A2+VNVBgaWYR24FdWjl7VNW83rnWftlhyzOZ/tBjezRiC2UkIzuxC8Top3IekN3vUf1WxA==
+
 url-template@^2.0.8:
   version "2.0.8"
   resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
diff --git a/frontend/package.json b/frontend/package.json
index a1531b9cc..19fe1d0ab 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -27,6 +27,7 @@
     "react-loading-icons": "^1.1.0",
     "react-loading-skeleton": "^3.1.0",
     "react-router-dom": "^6.3.0",
+    "react-tag-input-component": "^2.0.2",
     "react-toastify": "^9.1.3",
     "text-case": "^1.0.9",
     "truncate": "^3.0.0",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 2d1eeb7d1..7224f2e9c 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -36,6 +36,12 @@ const GeneralExportImport = lazy(() =>
   import("@/pages/GeneralSettings/ExportImport")
 );
 const GeneralSecurity = lazy(() => import("@/pages/GeneralSettings/Security"));
+const DataConnectors = lazy(() =>
+  import("@/pages/GeneralSettings/DataConnectors")
+);
+const DataConnectorSetup = lazy(() =>
+  import("@/pages/GeneralSettings/DataConnectors/Connectors")
+);
 const OnboardingFlow = lazy(() => import("@/pages/OnboardingFlow"));
 
 export default function App() {
@@ -103,6 +109,15 @@ export default function App() {
                 path="/settings/workspaces"
                 element={<ManagerRoute Component={AdminWorkspaces} />}
               />
+              <Route
+                path="/settings/data-connectors"
+                element={<ManagerRoute Component={DataConnectors} />}
+              />
+              <Route
+                path="/settings/data-connectors/:connector"
+                element={<ManagerRoute Component={DataConnectorSetup} />}
+              />
+
               {/* Onboarding Flow */}
               <Route path="/onboarding" element={<OnboardingFlow />} />
             </Routes>
diff --git a/frontend/src/components/DataConnectorOption/index.jsx b/frontend/src/components/DataConnectorOption/index.jsx
new file mode 100644
index 000000000..84af0ff1e
--- /dev/null
+++ b/frontend/src/components/DataConnectorOption/index.jsx
@@ -0,0 +1,39 @@
+import paths from "@/utils/paths";
+import ConnectorImages from "./media";
+
+export default function DataConnectorOption({ slug }) {
+  if (!DATA_CONNECTORS.hasOwnProperty(slug)) return null;
+  const { path, image, name, description, link } = DATA_CONNECTORS[slug];
+
+  return (
+    <a href={path}>
+      <label className="transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient">
+        <div className="flex items-center">
+          <img src={image} alt={name} className="h-10 w-10 rounded" />
+          <div className="ml-4 text-sm font-semibold">{name}</div>
+        </div>
+        <div className="mt-2 text-xs font-base text-white tracking-wide">
+          {description}
+        </div>
+        <a
+          href={link}
+          target="_blank"
+          className="mt-2 text-xs text-white font-medium underline"
+        >
+          {link}
+        </a>
+      </label>
+    </a>
+  );
+}
+
+export const DATA_CONNECTORS = {
+  github: {
+    name: "GitHub Repo",
+    path: paths.settings.dataConnectors.github(),
+    image: ConnectorImages.github,
+    description:
+      "Import an entire public or private Github repository in a single click.",
+    link: "https://github.com",
+  },
+};
diff --git a/frontend/src/components/DataConnectorOption/media/github.png b/frontend/src/components/DataConnectorOption/media/github.png
new file mode 100644
index 000000000..835221bab
Binary files /dev/null and b/frontend/src/components/DataConnectorOption/media/github.png differ
diff --git a/frontend/src/components/DataConnectorOption/media/index.js b/frontend/src/components/DataConnectorOption/media/index.js
new file mode 100644
index 000000000..a339328ef
--- /dev/null
+++ b/frontend/src/components/DataConnectorOption/media/index.js
@@ -0,0 +1,5 @@
+import Github from "./github.png";
+const ConnectorImages = {
+  github: Github,
+};
+export default ConnectorImages;
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx
index b83d695a0..f83a9e34c 100644
--- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx
@@ -33,7 +33,7 @@ export default function FileRow({
     try {
       setLoading(true);
       setLoadingMessage("This may take a while for large documents");
-      await System.deleteDocument(`${folderName}/${item.name}`, item);
+      await System.deleteDocument(`${folderName}/${item.name}`);
       await fetchKeys(true);
     } catch (error) {
       console.error("Failed to delete the document:", error);
@@ -60,7 +60,7 @@ export default function FileRow({
         selected ? "bg-sky-500/20" : ""
       } ${expanded ? "bg-sky-500/10" : ""}`}`}
     >
-      <div className="col-span-4 flex gap-x-[4px] items-center">
+      <div className="pl-4 col-span-4 flex gap-x-[4px] items-center">
         <div
           className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
           role="checkbox"
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx
index f2d5d5c71..c93a45cd3 100644
--- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx
@@ -1,7 +1,8 @@
 import { useState } from "react";
 import FileRow from "../FileRow";
-import { CaretDown, FolderNotch } from "@phosphor-icons/react";
+import { CaretDown, FolderNotch, Trash } from "@phosphor-icons/react";
 import { middleTruncate } from "@/utils/directories";
+import System from "@/models/system";
 
 export default function FolderRow({
   item,
@@ -12,8 +13,32 @@ export default function FolderRow({
   fetchKeys,
   setLoading,
   setLoadingMessage,
+  autoExpanded = false,
 }) {
-  const [expanded, setExpanded] = useState(true);
+  const [expanded, setExpanded] = useState(autoExpanded);
+
+  const onTrashClick = async (event) => {
+    event.stopPropagation();
+    if (
+      !window.confirm(
+        "Are you sure you want to delete this folder?\nThis will require you to re-upload and re-embed it.\nAny documents in this folder will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
+      )
+    ) {
+      return false;
+    }
+
+    try {
+      setLoading(true);
+      setLoadingMessage("This may take a while for large folders");
+      await System.deleteFolder(item.name);
+      await fetchKeys(true);
+    } catch (error) {
+      console.error("Failed to delete the document:", error);
+    }
+
+    if (selected) toggleSelection(item);
+    setLoading(false);
+  };
 
   const handleExpandClick = (event) => {
     event.stopPropagation();
@@ -30,7 +55,7 @@ export default function FolderRow({
       >
         <div className="col-span-4 flex gap-x-[4px] items-center">
           <div
-            className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
+            className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
             role="checkbox"
             aria-checked={selected}
             tabIndex={0}
@@ -46,7 +71,7 @@ export default function FolderRow({
             <CaretDown className="text-base font-bold w-4 h-4" />
           </div>
           <FolderNotch
-            className="text-base font-bold w-4 h-4 mr-[3px]"
+            className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
             weight="fill"
           />
           <p className="whitespace-nowrap overflow-show">
@@ -56,7 +81,14 @@ export default function FolderRow({
         <p className="col-span-2 pl-3.5" />
         <p className="col-span-2 pl-3" />
         <p className="col-span-2 pl-2" />
-        <div className="col-span-2 flex justify-end items-center" />
+        <div className="col-span-2 flex justify-end items-center">
+          {item.name !== "custom-documents" && (
+            <Trash
+              onClick={onTrashClick}
+              className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer"
+            />
+          )}
+        </div>
       </div>
       {expanded && (
         <div className="col-span-full">
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx
index 072010336..dcf625c5e 100644
--- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx
+++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx
@@ -106,6 +106,7 @@ export default function Directory({
                       isSelected={isSelected}
                       setLoading={setLoading}
                       setLoadingMessage={setLoadingMessage}
+                      autoExpanded={index === 0}
                     />
                   )
               )
diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx
index e599ee2b2..f4c9552e3 100644
--- a/frontend/src/components/SettingsSidebar/index.jsx
+++ b/frontend/src/components/SettingsSidebar/index.jsx
@@ -22,6 +22,7 @@ import {
   X,
   List,
   FileCode,
+  Plugs,
 } from "@phosphor-icons/react";
 import useUser from "@/hooks/useUser";
 import { USER_BACKGROUND_COLOR } from "@/utils/constants";
@@ -127,6 +128,11 @@ export default function SettingsSidebar() {
                       btnText="Vector Database"
                       icon={<Database className="h-5 w-5 flex-shrink-0" />}
                     />
+                    <Option
+                      href={paths.settings.dataConnectors.list()}
+                      btnText="Data Connectors"
+                      icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
+                    />
                   </>
                 )}
 
diff --git a/frontend/src/components/Sidebar/IndexCount.jsx b/frontend/src/components/Sidebar/IndexCount.jsx
deleted file mode 100644
index 9e0e126c6..000000000
--- a/frontend/src/components/Sidebar/IndexCount.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import pluralize from "pluralize";
-import React, { useEffect, useState } from "react";
-import System from "@/models/system";
-import { numberWithCommas } from "@/utils/numbers";
-
-export default function IndexCount() {
-  const [indexes, setIndexes] = useState(null);
-  useEffect(() => {
-    async function indexCount() {
-      setIndexes(await System.totalIndexes());
-    }
-    indexCount();
-  }, []);
-
-  if (indexes === null || indexes === 0) {
-    return (
-      <div className="flex w-full items-center justify-end gap-x-2">
-        <div className="flex items-center gap-x-1 px-2 rounded-full">
-          <p className="text-slate-400 leading-tight text-sm"></p>
-        </div>
-      </div>
-    );
-  }
-
-  return (
-    <div className="flex w-full items-center justify-end gap-x-2">
-      <div className="flex items-center gap-x-1  px-2 rounded-full">
-        <p className="text-slate-400 leading-tight text-sm">
-          {numberWithCommas(indexes)} {pluralize("vector", indexes)}
-        </p>
-      </div>
-    </div>
-  );
-}
diff --git a/frontend/src/components/Sidebar/LLMStatus.jsx b/frontend/src/components/Sidebar/LLMStatus.jsx
deleted file mode 100644
index 733dcb1e7..000000000
--- a/frontend/src/components/Sidebar/LLMStatus.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { WarningCircle, Circle } from "@phosphor-icons/react";
-import System from "@/models/system";
-
-export default function LLMStatus() {
-  const [status, setStatus] = useState(null);
-  useEffect(() => {
-    async function checkPing() {
-      setStatus(await System.ping());
-    }
-    checkPing();
-  }, []);
-
-  if (status === null) {
-    return (
-      <div className="flex w-full items-center justify-start gap-x-2">
-        <p className="text-slate-400 leading-loose text-sm">LLM</p>
-        <div className="flex items-center gap-x-1 border border-slate-400 px-2 rounded-full">
-          <p className="text-slate-400 leading-tight text-sm">unknown</p>
-          <Circle className="h-3 w-3 stroke-slate-700 fill-slate-400 animate-pulse" />
-        </div>
-      </div>
-    );
-  }
-
-  // TODO: add modal or toast on click to identify why this is broken
-  // need to likely start server.
-  if (status === false) {
-    return (
-      <div className="flex w-full items-center justify-end gap-x-2">
-        <p className="text-slate-400 leading-loose text-sm">LLM</p>
-        <div className="flex items-center gap-x-1 border border-red-400 px-2 bg-red-200 rounded-full">
-          <p className="text-red-700 leading-tight text-sm">offline</p>
-          <WarningCircle className="h-3 w-3 stroke-red-100 fill-red-400" />
-        </div>
-      </div>
-    );
-  }
-
-  return (
-    <div className="flex w-full items-center justify-end gap-x-2">
-      <p className="text-slate-400 leading-loose text-sm">LLM</p>
-      <div className="flex items-center gap-x-1 border border-slate-400 px-2 rounded-full">
-        <p className="text-slate-400 leading-tight text-sm">online</p>
-        <Circle className="h-3 w-3 stroke-green-100 fill-green-400 animate-pulse" />
-      </div>
-    </div>
-  );
-}
diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx
index ac92f4830..58af4048b 100644
--- a/frontend/src/components/Sidebar/index.jsx
+++ b/frontend/src/components/Sidebar/index.jsx
@@ -71,25 +71,6 @@ export default function Sidebar() {
               <ActiveWorkspaces />
             </div>
             <div className="flex flex-col flex-grow justify-end mb-2">
-              {/* <div className="flex flex-col gap-y-2">
-                <div className="w-full flex items-center justify-between">
-                  <LLMStatus />
-                  <IndexCount />
-                </div>
-                <a
-                  href={paths.feedback()}
-                  target="_blank"
-                  className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-transparent rounded-lg text-slate-200 justify-center items-center bg-stone-800 hover:bg-stone-900"
-                >
-                  <AtSign className="h-4 w-4" />
-                  <p className="text-slate-200 text-xs leading-loose font-semibold">
-                    Feedback form
-                  </p>
-                </a>
-                <ManagedHosting />
-                <LogoutButton />
-              </div> */}
-
               {/* Footer */}
               <div className="flex justify-center mt-2">
                 <div className="flex space-x-4">
diff --git a/frontend/src/index.css b/frontend/src/index.css
index f0f04bbcc..a7aef9a7e 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -385,3 +385,7 @@ dialog::backdrop {
     @apply border-blue-500 bg-blue-400/10 text-blue-800;
   }
 }
+
+.rti--container {
+  @apply !bg-zinc-900 !text-white !placeholder-white !placeholder-opacity-60 !text-sm !rounded-lg !p-2.5;
+}
diff --git a/frontend/src/models/dataConnector.js b/frontend/src/models/dataConnector.js
new file mode 100644
index 000000000..45d575024
--- /dev/null
+++ b/frontend/src/models/dataConnector.js
@@ -0,0 +1,47 @@
+import { API_BASE } from "@/utils/constants";
+import { baseHeaders } from "@/utils/request";
+import showToast from "@/utils/toast";
+
+const DataConnector = {
+  github: {
+    branches: async ({ repo, accessToken }) => {
+      return await fetch(`${API_BASE}/ext/github/branches`, {
+        method: "POST",
+        headers: baseHeaders(),
+        cache: "force-cache",
+        body: JSON.stringify({ repo, accessToken }),
+      })
+        .then((res) => res.json())
+        .then((res) => {
+          if (!res.success) throw new Error(res.reason);
+          return res.data;
+        })
+        .then((data) => {
+          return { branches: data?.branches || [], error: null };
+        })
+        .catch((e) => {
+          console.error(e);
+          showToast(e.message, "error");
+          return { branches: [], error: e.message };
+        });
+    },
+    collect: async function ({ repo, accessToken, branch, ignorePaths = [] }) {
+      return await fetch(`${API_BASE}/ext/github/repo`, {
+        method: "POST",
+        headers: baseHeaders(),
+        body: JSON.stringify({ repo, accessToken, branch, ignorePaths }),
+      })
+        .then((res) => res.json())
+        .then((res) => {
+          if (!res.success) throw new Error(res.reason);
+          return { data: res.data, error: null };
+        })
+        .catch((e) => {
+          console.error(e);
+          return { data: null, error: e.message };
+        });
+    },
+  },
+};
+
+export default DataConnector;
diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js
index 79c203d94..9b71a6055 100644
--- a/frontend/src/models/system.js
+++ b/frontend/src/models/system.js
@@ -1,5 +1,6 @@
 import { API_BASE, AUTH_TIMESTAMP } from "@/utils/constants";
 import { baseHeaders } from "@/utils/request";
+import DataConnector from "./dataConnector";
 
 const System = {
   ping: async function () {
@@ -133,11 +134,23 @@ const System = {
         return false;
       });
   },
-  deleteDocument: async (name, meta) => {
+  deleteDocument: async (name) => {
     return await fetch(`${API_BASE}/system/remove-document`, {
       method: "DELETE",
       headers: baseHeaders(),
-      body: JSON.stringify({ name, meta }),
+      body: JSON.stringify({ name }),
+    })
+      .then((res) => res.ok)
+      .catch((e) => {
+        console.error(e);
+        return false;
+      });
+  },
+  deleteFolder: async (name) => {
+    return await fetch(`${API_BASE}/system/remove-folder`, {
+      method: "DELETE",
+      headers: baseHeaders(),
+      body: JSON.stringify({ name }),
     })
       .then((res) => res.ok)
       .catch((e) => {
@@ -431,6 +444,7 @@ const System = {
         return { success: false, error: e.message };
       });
   },
+  dataConnectors: DataConnector,
 };
 
 export default System;
diff --git a/frontend/src/pages/GeneralSettings/DataConnectors/Connectors/Github/index.jsx b/frontend/src/pages/GeneralSettings/DataConnectors/Connectors/Github/index.jsx
new file mode 100644
index 000000000..fdcc8cb57
--- /dev/null
+++ b/frontend/src/pages/GeneralSettings/DataConnectors/Connectors/Github/index.jsx
@@ -0,0 +1,294 @@
+import React, { useEffect, useState } from "react";
+import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar";
+import { isMobile } from "react-device-detect";
+import { DATA_CONNECTORS } from "@/components/DataConnectorOption";
+import System from "@/models/system";
+import { Info } from "@phosphor-icons/react/dist/ssr";
+import showToast from "@/utils/toast";
+import pluralize from "pluralize";
+import { TagsInput } from "react-tag-input-component";
+
+const DEFAULT_BRANCHES = ["main", "master"];
+export default function GithubConnectorSetup() {
+  const { image } = DATA_CONNECTORS.github;
+  const [loading, setLoading] = useState(false);
+  const [repo, setRepo] = useState(null);
+  const [accessToken, setAccessToken] = useState(null);
+  const [ignores, setIgnores] = useState([]);
+
+  const [settings, setSettings] = useState({
+    repo: null,
+    accessToken: null,
+  });
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    const form = new FormData(e.target);
+
+    try {
+      setLoading(true);
+      showToast(
+        "Fetching all files for repo - this may take a while.",
+        "info",
+        { clear: true, autoClose: false }
+      );
+      const { data, error } = await System.dataConnectors.github.collect({
+        repo: form.get("repo"),
+        accessToken: form.get("accessToken"),
+        branch: form.get("branch"),
+        ignorePaths: ignores,
+      });
+
+      if (!!error) {
+        showToast(error, "error", { clear: true });
+        setLoading(false);
+        return;
+      }
+
+      showToast(
+        `${data.files} ${pluralize("file", data.files)} collected from ${
+          data.author
+        }/${data.repo}:${data.branch}. Output folder is ${data.destination}.`,
+        "success",
+        { clear: true }
+      );
+      e.target.reset();
+      setLoading(false);
+      return;
+    } catch (e) {
+      console.error(e);
+      showToast(e.message, "error", { clear: true });
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
+      {!isMobile && <Sidebar />}
+      <div
+        style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
+        className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
+      >
+        {isMobile && <SidebarMobileHeader />}
+        <div className="flex w-full">
+          <div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
+            <div className="flex w-full gap-x-4 items-center  pb-6 border-white border-b-2 border-opacity-10">
+              <img src={image} alt="Github" className="rounded-lg h-16 w-16" />
+              <div className="w-full flex flex-col gap-y-1">
+                <div className="items-center flex gap-x-4">
+                  <p className="text-2xl font-semibold text-white">
+                    Import GitHub Repository
+                  </p>
+                </div>
+                <p className="text-sm font-base text-white text-opacity-60">
+                  Import all files from a public or private Github repository
+                  and have its files be available in your workspace.
+                </p>
+              </div>
+            </div>
+
+            <form className="w-full" onSubmit={handleSubmit}>
+              {!accessToken && (
+                <div className="flex flex-col gap-y-1 py-4 ">
+                  <div className="flex flex-col w-fit gap-y-2 bg-blue-600/20 rounded-lg px-4 py-2">
+                    <div className="flex items-center gap-x-2">
+                      <Info size={20} className="shrink-0 text-blue-400" />
+                      <p className="text-blue-400 text-sm">
+                        Trying to collect a GitHub repo without a{" "}
+                        <a
+                          href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
+                          rel="noreferrer"
+                          target="_blank"
+                          className="underline"
+                        >
+                          Personal Access Token
+                        </a>{" "}
+                        will fail to collect all files due to GitHub API limits.
+                      </p>
+                    </div>
+                    <a
+                      href="https://github.com/settings/personal-access-tokens/new"
+                      rel="noreferrer"
+                      target="_blank"
+                      className="text-blue-400 hover:underline"
+                    >
+                      Create a temporary Access Token for this data connector
+                      &rarr;
+                    </a>
+                  </div>
+                </div>
+              )}
+
+              <div className="w-full flex flex-col py-2">
+                <div className="w-full flex items-center gap-4">
+                  <div className="flex flex-col w-60">
+                    <div className="flex flex-col gap-y-1 mb-4">
+                      <label className="text-white text-sm font-semibold block">
+                        GitHub Repo URL
+                      </label>
+                      <p className="text-xs text-zinc-300">
+                        Url of the GitHub repo you wish to collect.
+                      </p>
+                    </div>
+                    <input
+                      type="url"
+                      name="repo"
+                      className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
+                      placeholder="https://github.com/Mintplex-Labs/anything-llm"
+                      required={true}
+                      autoComplete="off"
+                      onChange={(e) => setRepo(e.target.value)}
+                      onBlur={() => setSettings({ ...settings, repo })}
+                      spellCheck={false}
+                    />
+                  </div>
+                  <div className="flex flex-col w-60">
+                    <div className="flex flex-col gap-y-1 mb-4">
+                      <label className="text-white text-sm block flex gap-x-2 items-center">
+                        <p className="font-semibold ">Github Access Token</p>{" "}
+                        <p className="text-xs text-zinc-300 font-base!">
+                          <i>optional</i>
+                        </p>
+                      </label>
+                      <p className="text-xs text-zinc-300 flex gap-x-2">
+                        Access Token to prevent rate limiting.
+                      </p>
+                    </div>
+                    <input
+                      type="text"
+                      name="accessToken"
+                      className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
+                      placeholder="github_pat_1234_abcdefg"
+                      required={false}
+                      autoComplete="off"
+                      spellCheck={false}
+                      onChange={(e) => setAccessToken(e.target.value)}
+                      onBlur={() => setSettings({ ...settings, accessToken })}
+                    />
+                  </div>
+                  <GitHubBranchSelection
+                    repo={settings.repo}
+                    accessToken={settings.accessToken}
+                  />
+                </div>
+
+                <div className="flex flex-col w-1/2 py-4">
+                  <div className="flex flex-col gap-y-1 mb-4">
+                    <label className="text-white text-sm block flex gap-x-2 items-center">
+                      <p className="font-semibold ">File Ignores</p>
+                    </label>
+                    <p className="text-xs text-zinc-300 flex gap-x-2">
+                      List in .gitignore format to ignore specific files during
+                      collection. Press enter after each entry you want to save.
+                    </p>
+                  </div>
+                  <TagsInput
+                    value={ignores}
+                    onChange={setIgnores}
+                    name="ignores"
+                    placeholder="!*.js, images/*, .DS_Store, bin/*"
+                    classNames={{
+                      tag: "bg-blue-300/10 text-zinc-800 m-1",
+                      input:
+                        "flex bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white p-2.5",
+                    }}
+                  />
+                </div>
+              </div>
+
+              <div className="flex flex-col gap-y-2 w-fit">
+                <button
+                  type="submit"
+                  disabled={loading}
+                  className="mt-2 text-lg w-fit border border-slate-200 px-4 py-1 rounded-lg text-slate-200 items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:bg-slate-200 disabled:text-slate-800"
+                >
+                  {loading
+                    ? "Collecting files..."
+                    : "Collect all files from GitHub repo"}
+                </button>
+                {loading && (
+                  <p className="text-xs text-zinc-300">
+                    Once complete, all files will be available for embedding
+                    into workspaces in the document picker.
+                  </p>
+                )}
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function GitHubBranchSelection({ repo, accessToken }) {
+  const [allBranches, setAllBranches] = useState(DEFAULT_BRANCHES);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    async function fetchAllBranches() {
+      if (!repo) {
+        setAllBranches(DEFAULT_BRANCHES);
+        setLoading(false);
+        return;
+      }
+
+      setLoading(true);
+      const { branches } = await System.dataConnectors.github.branches({
+        repo,
+        accessToken,
+      });
+      setAllBranches(branches.length > 0 ? branches : DEFAULT_BRANCHES);
+      setLoading(false);
+    }
+    fetchAllBranches();
+  }, [repo, accessToken]);
+
+  if (loading) {
+    return (
+      <div className="flex flex-col w-60">
+        <div className="flex flex-col gap-y-1 mb-4">
+          <label className="text-white text-sm font-semibold block">
+            Branch
+          </label>
+          <p className="text-xs text-zinc-300">
+            Branch you wish to collect files of
+          </p>
+        </div>
+        <select
+          name="branch"
+          required={true}
+          className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
+        >
+          <option disabled={true} selected={true}>
+            -- loading available models --
+          </option>
+        </select>
+      </div>
+    );
+  }
+
+  return (
+    <div className="flex flex-col w-60">
+      <div className="flex flex-col gap-y-1 mb-4">
+        <label className="text-white text-sm font-semibold block">Branch</label>
+        <p className="text-xs text-zinc-300">
+          Branch you wish to collect files of
+        </p>
+      </div>
+      <select
+        name="branch"
+        required={true}
+        className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
+      >
+        {allBranches.map((branch) => {
+          return (
+            <option key={branch} value={branch}>
+              {branch}
+            </option>
+          );
+        })}
+      </select>
+    </div>
+  );
+}
diff --git a/frontend/src/pages/GeneralSettings/DataConnectors/Connectors/index.jsx b/frontend/src/pages/GeneralSettings/DataConnectors/Connectors/index.jsx
new file mode 100644
index 000000000..cbd66f08a
--- /dev/null
+++ b/frontend/src/pages/GeneralSettings/DataConnectors/Connectors/index.jsx
@@ -0,0 +1,19 @@
+import paths from "@/utils/paths";
+import { lazy } from "react";
+import { useParams } from "react-router-dom";
+const Github = lazy(() => import("./Github"));
+
+const CONNECTORS = {
+  github: Github,
+};
+
+export default function DataConnectorSetup() {
+  const { connector } = useParams();
+  if (!connector || !CONNECTORS.hasOwnProperty(connector)) {
+    window.location = paths.home();
+    return;
+  }
+
+  const Page = CONNECTORS[connector];
+  return <Page />;
+}
diff --git a/frontend/src/pages/GeneralSettings/DataConnectors/index.jsx b/frontend/src/pages/GeneralSettings/DataConnectors/index.jsx
new file mode 100644
index 000000000..76dc13d0a
--- /dev/null
+++ b/frontend/src/pages/GeneralSettings/DataConnectors/index.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar";
+import { isMobile } from "react-device-detect";
+import DataConnectorOption from "@/components/DataConnectorOption";
+
+export default function DataConnectors() {
+  return (
+    <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
+      {!isMobile && <Sidebar />}
+      <div
+        style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
+        className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
+      >
+        {isMobile && <SidebarMobileHeader />}
+        <div className="flex w-full">
+          <div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
+            <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
+              <div className="items-center flex gap-x-4">
+                <p className="text-2xl font-semibold text-white">
+                  Data Connectors
+                </p>
+              </div>
+              <p className="text-sm font-base text-white text-opacity-60">
+                Verified data connectors allow you to add more content to your
+                AnythingLLM workspaces with no custom code or complexity.
+                <br />
+                Guaranteed to work with your AnythingLLM instance.
+              </p>
+            </div>
+            <div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full">
+              <DataConnectorOption slug="github" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js
index bfeb8b1bd..c21c1500b 100644
--- a/frontend/src/utils/paths.js
+++ b/frontend/src/utils/paths.js
@@ -76,5 +76,13 @@ export default {
     apiKeys: () => {
       return "/settings/api-keys";
     },
+    dataConnectors: {
+      list: () => {
+        return "/settings/data-connectors";
+      },
+      github: () => {
+        return "/settings/data-connectors/github";
+      },
+    },
   },
 };
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index fdb7aae69..1d0639f44 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2061,6 +2061,11 @@ react-router@6.12.1:
   dependencies:
     "@remix-run/router" "1.6.3"
 
+react-tag-input-component@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/react-tag-input-component/-/react-tag-input-component-2.0.2.tgz#f62f013c6a535141dd1c6c3a88858223170150f1"
+  integrity sha512-dydI9luVwwv9vrjE5u1TTnkcOVkOVL6mhFti8r6hLi78V2F2EKWQOLptURz79UYbDHLSk6tnbvGl8FE+sMpADg==
+
 react-toastify@^9.1.3:
   version "9.1.3"
   resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff"
diff --git a/server/endpoints/extensions/index.js b/server/endpoints/extensions/index.js
new file mode 100644
index 000000000..fc545ce3c
--- /dev/null
+++ b/server/endpoints/extensions/index.js
@@ -0,0 +1,53 @@
+const { Telemetry } = require("../../models/telemetry");
+const {
+  forwardExtensionRequest,
+} = require("../../utils/files/documentProcessor");
+const {
+  flexUserRoleValid,
+} = require("../../utils/middleware/multiUserProtected");
+const { validatedRequest } = require("../../utils/middleware/validatedRequest");
+
+function extensionEndpoints(app) {
+  if (!app) return;
+
+  app.post(
+    "/ext/github/branches",
+    [validatedRequest, flexUserRoleValid],
+    async (request, response) => {
+      try {
+        const responseFromProcessor = await forwardExtensionRequest({
+          endpoint: "/ext/github-repo/branches",
+          method: "POST",
+          body: request.body,
+        });
+        response.status(200).json(responseFromProcessor);
+      } catch (e) {
+        console.error(e);
+        response.sendStatus(500).end();
+      }
+    }
+  );
+
+  app.post(
+    "/ext/github/repo",
+    [validatedRequest, flexUserRoleValid],
+    async (request, response) => {
+      try {
+        const responseFromProcessor = await forwardExtensionRequest({
+          endpoint: "/ext/github-repo",
+          method: "POST",
+          body: request.body,
+        });
+        await Telemetry.sendTelemetry("extension_invoked", {
+          type: "github_repo",
+        });
+        response.status(200).json(responseFromProcessor);
+      } catch (e) {
+        console.error(e);
+        response.sendStatus(500).end();
+      }
+    }
+  );
+}
+
+module.exports = { extensionEndpoints };
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index 790305cff..fe54f6d2b 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -7,7 +7,7 @@ const {
   checkProcessorAlive,
   acceptedFileTypes,
 } = require("../utils/files/documentProcessor");
-const { purgeDocument } = require("../utils/files/purgeDocument");
+const { purgeDocument, purgeFolder } = require("../utils/files/purgeDocument");
 const { getVectorDbClass } = require("../utils/helpers");
 const { updateENV, dumpENV } = require("../utils/helpers/updateENV");
 const {
@@ -196,8 +196,23 @@ function systemEndpoints(app) {
     [validatedRequest],
     async (request, response) => {
       try {
-        const { name, meta } = reqBody(request);
-        await purgeDocument(name, meta);
+        const { name } = reqBody(request);
+        await purgeDocument(name);
+        response.sendStatus(200).end();
+      } catch (e) {
+        console.log(e.message, e);
+        response.sendStatus(500).end();
+      }
+    }
+  );
+
+  app.delete(
+    "/system/remove-folder",
+    [validatedRequest],
+    async (request, response) => {
+      try {
+        const { name } = reqBody(request);
+        await purgeFolder(name);
         response.sendStatus(200).end();
       } catch (e) {
         console.log(e.message, e);
diff --git a/server/index.js b/server/index.js
index d6999dd35..1a106053c 100644
--- a/server/index.js
+++ b/server/index.js
@@ -18,6 +18,7 @@ const { utilEndpoints } = require("./endpoints/utils");
 const { Telemetry } = require("./models/telemetry");
 const { developerEndpoints } = require("./endpoints/api");
 const setupTelemetry = require("./utils/telemetry");
+const { extensionEndpoints } = require("./endpoints/extensions");
 const app = express();
 const apiRouter = express.Router();
 const FILE_LIMIT = "3GB";
@@ -34,6 +35,7 @@ app.use(
 
 app.use("/api", apiRouter);
 systemEndpoints(apiRouter);
+extensionEndpoints(apiRouter);
 workspaceEndpoints(apiRouter);
 chatEndpoints(apiRouter);
 adminEndpoints(apiRouter);
diff --git a/server/utils/files/documentProcessor.js b/server/utils/files/documentProcessor.js
index ea47a1582..5239a8708 100644
--- a/server/utils/files/documentProcessor.js
+++ b/server/utils/files/documentProcessor.js
@@ -59,9 +59,32 @@ async function processLink(link = "") {
     });
 }
 
+// We will not ever expose the document processor to the frontend API so instead we relay
+// all requests through the server. You can use this function to directly expose a specific endpoint
+// on the document processor.
+async function forwardExtensionRequest({ endpoint, method, body }) {
+  return await fetch(`${PROCESSOR_API}${endpoint}`, {
+    method,
+    body, // Stringified JSON!
+    headers: {
+      "Content-Type": "application/json",
+    },
+  })
+    .then((res) => {
+      if (!res.ok) throw new Error("Response could not be completed");
+      return res.json();
+    })
+    .then((res) => res)
+    .catch((e) => {
+      console.log(e.message);
+      return { success: false, data: {}, reason: e.message };
+    });
+}
+
 module.exports = {
   checkProcessorAlive,
   processDocument,
   processLink,
   acceptedFileTypes,
+  forwardExtensionRequest,
 };
diff --git a/server/utils/files/index.js b/server/utils/files/index.js
index 83505f8b4..c86221876 100644
--- a/server/utils/files/index.js
+++ b/server/utils/files/index.js
@@ -144,18 +144,14 @@ async function storeVectorResult(vectorData = [], filename = null) {
 // Purges a file from the documents/ folder.
 async function purgeSourceDocument(filename = null) {
   if (!filename) return;
-  console.log(`Purging document of ${filename}.`);
+  console.log(`Purging source document of ${filename}.`);
 
   const filePath =
     process.env.NODE_ENV === "development"
       ? path.resolve(__dirname, `../../storage/documents`, filename)
       : path.resolve(process.env.STORAGE_DIR, `documents`, filename);
 
-  if (!fs.existsSync(filePath)) {
-    console.log(`Could not located cachefile for ${filename}`, filePath);
-    return;
-  }
-
+  if (!fs.existsSync(filePath)) return;
   fs.rmSync(filePath);
   return;
 }
@@ -163,7 +159,7 @@ async function purgeSourceDocument(filename = null) {
 // Purges a vector-cache file from the vector-cache/ folder.
 async function purgeVectorCache(filename = null) {
   if (!filename) return;
-  console.log(`Purging cached vectorized results of ${filename}.`);
+  console.log(`Purging vector-cache of ${filename}.`);
 
   const digest = uuidv5(filename, uuidv5.URL);
   const filePath =
@@ -171,11 +167,7 @@ async function purgeVectorCache(filename = null) {
       ? path.resolve(__dirname, `../../storage/vector-cache`, `${digest}.json`)
       : path.resolve(process.env.STORAGE_DIR, `vector-cache`, `${digest}.json`);
 
-  if (!fs.existsSync(filePath)) {
-    console.log(`Could not located cache file for ${filename}`, filePath);
-    return;
-  }
-
+  if (!fs.existsSync(filePath)) return;
   fs.rmSync(filePath);
   return;
 }
diff --git a/server/utils/files/purgeDocument.js b/server/utils/files/purgeDocument.js
index a584a4261..27fe14710 100644
--- a/server/utils/files/purgeDocument.js
+++ b/server/utils/files/purgeDocument.js
@@ -1,8 +1,11 @@
+const fs = require("fs");
+const path = require("path");
+
 const { purgeVectorCache, purgeSourceDocument } = require(".");
 const { Document } = require("../../models/documents");
 const { Workspace } = require("../../models/workspace");
 
-async function purgeDocument(filename, meta) {
+async function purgeDocument(filename) {
   const workspaces = await Workspace.where();
   for (const workspace of workspaces) {
     await Document.removeDocuments(workspace, [filename]);
@@ -12,6 +15,45 @@ async function purgeDocument(filename, meta) {
   return;
 }
 
+async function purgeFolder(folderName) {
+  if (folderName === "custom-documents") return;
+  const documentsFolder =
+    process.env.NODE_ENV === "development"
+      ? path.resolve(__dirname, `../../storage/documents`)
+      : path.resolve(process.env.STORAGE_DIR, `documents`);
+
+  const folderPath = path.resolve(documentsFolder, folderName);
+  const filenames = fs
+    .readdirSync(folderPath)
+    .map((file) => path.join(folderName, file));
+  const workspaces = await Workspace.where();
+
+  const purgePromises = [];
+  // Remove associated Vector-cache files
+  for (const filename of filenames) {
+    const rmVectorCache = () =>
+      new Promise((resolve) =>
+        purgeVectorCache(filename).then(() => resolve(true))
+      );
+    purgePromises.push(rmVectorCache);
+  }
+
+  // Remove workspace document associations
+  for (const workspace of workspaces) {
+    const rmWorkspaceDoc = () =>
+      new Promise((resolve) =>
+        Document.removeDocuments(workspace, filenames).then(() => resolve(true))
+      );
+    purgePromises.push(rmWorkspaceDoc);
+  }
+
+  await Promise.all(purgePromises.flat().map((f) => f()));
+  fs.rmSync(folderPath, { recursive: true }); // Delete root document and source files.
+
+  return;
+}
+
 module.exports = {
   purgeDocument,
+  purgeFolder,
 };