diff --git a/package.json b/package.json
index b30427360d84d41e403ce3a669a7422ab1881f4c..5739c7e7b632a3f61ebb7c48c22f808353193554 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "@testing-library/react": "^11.1.0",
     "@testing-library/user-event": "^12.1.10",
     "@types/jest": "^26.0.15",
+    "@types/js-yaml": "^4.0.5",
     "@types/node": "^12.0.0",
     "@types/react-dom": "^17.0.0",
     "axios": "^0.21.1",
@@ -28,8 +29,8 @@
     "react-hot-toast": "^2.0.0",
     "react-markdown": "^7.0.1",
     "react-redux": "^7.2.4",
-    "react-router-dom": "6.2.1",
     "react-router": "6.2.1",
+    "react-router-dom": "6.2.1",
     "react-scripts": "4.0.3",
     "react-simple-code-editor": "^0.11.0",
     "react-table": "^7.7.0",
diff --git a/public/assets/monitoring.svg b/public/assets/monitoring.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fa9380825724e61e8f04f7effc0d83c41506ea0a
--- /dev/null
+++ b/public/assets/monitoring.svg
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:url(#SVGID_1_);}
+</style>
+<g>
+	
+		<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-0.7593" y1="513.0059" x2="1.0811" y2="513.0059" gradientTransform="matrix(1.431349e-14 -254.0268 254.0268 1.690345e-14 -130061.25 434.0558)">
+		<stop  offset="0" style="stop-color:#FFF100"/>
+		<stop  offset="1" style="stop-color:#F05A28"/>
+	</linearGradient>
+	<path class="st0" d="M490.8,226c-0.8-8.6-2.3-18.5-5.1-29.5c-2.8-10.9-7.1-22.8-13.3-35.3c-6.2-12.4-14.2-25.2-24.7-37.8
+		c-4.1-4.9-8.6-9.7-13.4-14.4c7.2-28.6-8.7-53.5-8.7-53.5c-27.5-1.7-45,8.6-51.5,13.3c-1.1-0.4-2.1-1-3.2-1.4
+		c-4.7-1.8-9.5-3.7-14.5-5.2c-4.9-1.6-10-3-15.2-4.2c-5.2-1.3-10.4-2.3-15.8-3.1c-1-0.1-1.8-0.3-2.8-0.4C310.6,16.1,276.1,0,276.1,0
+		c-38.5,24.4-45.7,58.5-45.7,58.5s-0.1,0.7-0.4,2c-2.1,0.6-4.2,1.3-6.3,1.8c-3,0.8-5.9,2-8.7,3.1c-3,1.1-5.8,2.3-8.7,3.5
+		c-5.8,2.5-11.6,5.4-17.2,8.5c-5.5,3.1-10.9,6.5-16.1,10c-0.7-0.3-1.4-0.6-1.4-0.6C118.2,66.6,70.9,91,70.9,91
+		c-4.4,56.7,21.3,92.4,26.4,98.9c-1.3,3.5-2.4,7.1-3.5,10.6c-3.9,12.8-6.9,26-8.7,39.6c-0.3,2-0.6,3.9-0.7,5.9
+		c-49.4,24.4-63.9,74.2-63.9,74.2c41,47.3,89,50.2,89,50.2l0.1-0.1c6.1,10.9,13.1,21.2,21,30.9c3.4,4.1,6.8,7.9,10.4,11.7
+		c-15,42.9,2.1,78.4,2.1,78.4c45.7,1.7,75.7-20,82.1-25c4.5,1.6,9.2,3,13.8,4.1c14.1,3.7,28.5,5.8,42.9,6.3
+		c3.5,0.1,7.2,0.3,10.7,0.1h1.7h1.1h2.3l2.3-0.1v0.1c21.6,30.7,59.4,35.1,59.4,35.1c26.9-28.4,28.5-56.6,28.5-62.6c0,0,0-0.1,0-0.4
+		c0-0.6,0-0.8,0-0.8c0-0.4,0-0.8,0-1.3c5.6-4,11-8.2,16.1-12.8c10.7-9.7,20.2-20.9,28.1-32.9c0.7-1.1,1.4-2.3,2.1-3.4
+		c30.5,1.7,52-18.9,52-18.9c-5.1-31.7-23.1-47.3-26.9-50.2c0,0-0.1-0.1-0.4-0.3c-0.3-0.1-0.3-0.3-0.3-0.3c-0.1-0.1-0.4-0.3-0.7-0.4
+		c0.1-2,0.3-3.8,0.4-5.8c0.3-3.4,0.3-6.9,0.3-10.3V309v-1.3v-0.7c0-0.8,0-0.6,0-0.8l-0.1-2.1l-0.1-2.8c0-1-0.1-1.8-0.3-2.7
+		c-0.1-0.8-0.1-1.8-0.3-2.7l-0.3-2.7l-0.4-2.7c-0.6-3.5-1.1-6.9-2-10.4c-3.2-13.7-8.6-26.7-15.5-38.4c-7.1-11.7-15.8-22-25.8-30.7
+		c-9.9-8.7-21-15.8-32.6-21c-11.7-5.2-23.8-8.6-36-10.2c-6.1-0.8-12.1-1.1-18.2-1h-2.3h-0.6h-0.7h-1l-2.3,0.1
+		c-0.8,0-1.7,0.1-2.4,0.1c-3.1,0.3-6.2,0.7-9.2,1.3c-12.1,2.3-23.6,6.6-33.6,12.7c-10,6.1-18.8,13.5-25.8,22
+		c-7.1,8.5-12.6,17.9-16.4,27.6s-5.9,19.9-6.5,29.6c-0.1,2.4-0.1,4.9-0.1,7.3c0,0.6,0,1.3,0,1.8l0.1,2c0.1,1.1,0.1,2.4,0.3,3.5
+		c0.4,4.9,1.4,9.7,2.7,14.2c2.7,9.2,6.9,17.5,12.1,24.5c5.2,7.1,11.6,12.8,18.2,17.5c6.6,4.5,13.8,7.8,20.9,9.9
+		c7.1,2.1,14.1,3,20.7,3c0.8,0,1.7,0,2.4,0c0.4,0,0.8,0,1.3,0s0.8,0,1.3-0.1c0.7,0,1.4-0.1,2.1-0.1l0.6-0.1l0.7-0.1
+		c0.4,0,0.8-0.1,1.3-0.1c0.8-0.1,1.6-0.3,2.4-0.4c0.8-0.1,1.6-0.3,2.3-0.6c1.6-0.3,3-0.8,4.4-1.3c2.8-1,5.6-2.1,8-3.4
+		c2.5-1.3,4.8-2.8,7.1-4.2c0.6-0.4,1.3-0.8,1.8-1.4c2.3-1.8,2.7-5.2,0.8-7.5c-1.6-2-4.4-2.5-6.6-1.3c-0.6,0.3-1.1,0.6-1.7,0.8
+		c-2,1-3.9,1.8-6.1,2.5c-2.1,0.7-4.4,1.3-6.6,1.7c-1.1,0.1-2.3,0.3-3.5,0.4c-0.6,0-1.1,0.1-1.8,0.1c-0.6,0-1.3,0-1.7,0
+		c-0.6,0-1.1,0-1.7,0c-0.7,0-1.4,0-2.1-0.1c0,0-0.4,0-0.1,0h-0.3h-0.4c-0.3,0-0.7,0-1-0.1c-0.7-0.1-1.3-0.1-2-0.3
+		c-5.2-0.7-10.4-2.3-15.4-4.5c-5.1-2.3-9.9-5.4-14.2-9.3c-4.4-4-8.2-8.6-11.1-14c-3-5.4-5.1-11.3-6.1-17.5c-0.4-3.1-0.7-6.3-0.6-9.5
+		c0-0.8,0.1-1.7,0.1-2.5c0,0.3,0-0.1,0-0.1v-0.3v-0.7c0-0.4,0.1-0.8,0.1-1.3c0.1-1.7,0.4-3.4,0.7-5.1c2.4-13.5,9.2-26.8,19.6-36.8
+		c2.7-2.5,5.5-4.8,8.5-6.9c3-2.1,6.2-3.9,9.6-5.5c3.4-1.6,6.8-2.8,10.4-3.8c3.5-1,7.2-1.6,11-2c1.8-0.1,3.7-0.3,5.6-0.3
+		c0.6,0,0.8,0,1.3,0h1.6h1c0.4,0,0,0,0.1,0h0.4l1.6,0.1c4.1,0.3,8,0.8,12,1.8c7.9,1.7,15.7,4.7,22.8,8.6c14.4,8,26.7,20.5,34.1,35.4
+		c3.8,7.5,6.5,15.5,7.8,23.8c0.3,2.1,0.6,4.2,0.7,6.3l0.1,1.6l0.1,1.6c0,0.6,0,1.1,0,1.6c0,0.6,0,1.1,0,1.6v1.4v1.6
+		c0,1-0.1,2.7-0.1,3.7c-0.1,2.3-0.4,4.7-0.7,6.9c-0.3,2.3-0.7,4.5-1.1,6.8c-0.4,2.3-1,4.5-1.6,6.6c-1.1,4.4-2.5,8.7-4.2,13.1
+		c-3.4,8.5-7.9,16.6-13.3,24.1c-10.9,15-25.7,27.1-42.6,34.8c-8.5,3.8-17.3,6.6-26.5,8c-4.5,0.8-9.2,1.3-13.8,1.4h-0.8h-0.7h-1.6
+		h-2.3h-1.1c0.6,0-0.1,0-0.1,0h-0.4c-2.5,0-4.9-0.1-7.5-0.4c-9.9-0.7-19.6-2.5-29.2-5.2c-9.5-2.7-18.6-6.5-27.4-11
+		c-17.3-9.3-33-22-45.1-37.4c-6.1-7.6-11.4-15.9-15.8-24.5c-4.4-8.6-7.9-17.8-10.4-26.9c-2.5-9.3-4.1-18.8-4.8-28.4l-0.1-1.8v-0.4
+		v-0.4v-0.8v-1.6v-0.4v-0.6v-1.1V268v-0.4c0,0,0,0.1,0-0.1v-0.8c0-1.1,0-2.4,0-3.5c0.1-4.7,0.6-9.6,1.1-14.4
+		c0.6-4.8,1.4-9.7,2.4-14.5c1-4.8,2.1-9.6,3.5-14.4c2.7-9.5,6.1-18.6,10-27.2c8-17.2,18.5-32.6,31-44.9c3.1-3.1,6.3-5.9,9.7-8.7
+		c3.4-2.7,6.9-5.2,10.6-7.6c3.5-2.4,7.3-4.5,11.1-6.5c1.8-1,3.8-2,5.8-2.8c1-0.4,2-0.8,3-1.3c1-0.4,2-0.8,3-1.3
+		c3.9-1.7,8-3.1,12.3-4.4c1-0.3,2.1-0.6,3.1-1c1-0.3,2.1-0.6,3.1-0.8c2.1-0.6,4.2-1.1,6.3-1.6c1-0.3,2.1-0.4,3.2-0.7
+		c1.1-0.3,2.1-0.4,3.2-0.7c1.1-0.1,2.1-0.4,3.2-0.6l1.6-0.3l1.7-0.3c1.1-0.1,2.1-0.3,3.2-0.4c1.3-0.1,2.4-0.3,3.7-0.4
+		c1-0.1,2.7-0.3,3.7-0.4c0.7-0.1,1.6-0.1,2.3-0.3l1.6-0.1l0.7-0.1h0.8c1.3-0.1,2.4-0.1,3.7-0.3l1.8-0.1c0,0,0.7,0,0.1,0h0.4h0.8
+		c1,0,2.1-0.1,3.1-0.1c4.1-0.1,8.3-0.1,12.4,0c8.2,0.3,16.2,1.3,24,2.7c15.7,3,30.3,7.9,43.7,14.5c13.4,6.5,25.2,14.5,35.7,23.3
+		c0.7,0.6,1.3,1.1,2,1.7c0.6,0.6,1.3,1.1,1.8,1.7c1.3,1.1,2.4,2.3,3.7,3.4c1.3,1.1,2.4,2.3,3.5,3.4c1.1,1.1,2.3,2.3,3.4,3.5
+		c4.4,4.7,8.5,9.3,12.1,14.1c7.3,9.5,13.3,19,17.9,28.1c0.3,0.6,0.6,1.1,0.8,1.7s0.6,1.1,0.8,1.7c0.6,1.1,1.1,2.3,1.6,3.4
+		c0.6,1.1,1,2.1,1.6,3.2c0.4,1.1,1,2.1,1.4,3.2c1.7,4.2,3.4,8.3,4.7,12.1c2.1,6.2,3.7,11.7,4.9,16.5c0.4,2,2.3,3.2,4.2,3
+		c2.1-0.1,3.7-1.8,3.7-3.9C491.7,238.9,491.5,232.9,490.8,226z"/>
+</g>
+</svg>
diff --git a/public/markdown/monitoring.md b/public/markdown/monitoring.md
new file mode 100644
index 0000000000000000000000000000000000000000..8d03535b686dc41c12e46c8cb1c5620f76a3a1c8
--- /dev/null
+++ b/public/markdown/monitoring.md
@@ -0,0 +1 @@
+Monitor your system with Grafana
diff --git a/public/markdown/support.md b/public/markdown/support.md
new file mode 100644
index 0000000000000000000000000000000000000000..efd389bacae0b0d4ca8788b4bb9846a7bd915132
--- /dev/null
+++ b/public/markdown/support.md
@@ -0,0 +1 @@
+Access documentation website
diff --git a/src/App.tsx b/src/App.tsx
index b9797b0c763a2b654774ee5b4930004bfa9cfb48..7d2d7e1131480e90e50a7d6a249285a1bdce83bb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,7 +4,7 @@ import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
 import { Toaster } from 'react-hot-toast';
 
 import { useAuth } from 'src/services/auth';
-import { Dashboard, Users, Login } from './modules';
+import { Dashboard, Users, Login, Apps, AppSingle } from './modules';
 import { Layout } from './components';
 import { LoginCallback } from './modules/login/LoginCallback';
 
@@ -43,7 +43,11 @@ function App() {
             <Routes>
               <Route path="/dashboard" element={<Dashboard />} />
               <Route path="/users" element={<ProtectedRoute />}>
-                <Route path="/users" element={<Users />} />
+                <Route index element={<Users />} />
+              </Route>
+              <Route path="/apps" element={<ProtectedRoute />}>
+                <Route path=":slug" element={<AppSingle />} />
+                <Route index element={<Apps />} />
               </Route>
               <Route path="*" element={<Navigate to="/dashboard" />} />
             </Routes>
diff --git a/src/components/Form/Checkbox/Checkbox.tsx b/src/components/Form/Checkbox/Checkbox.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e31c20c725f2144bca2ae19d850bfb4e764f4c39
--- /dev/null
+++ b/src/components/Form/Checkbox/Checkbox.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { useController } from 'react-hook-form';
+
+/* eslint-disable react/react-in-jsx-scope */
+export const Checkbox = ({ control, name, label, ...props }: CheckboxProps) => {
+  const {
+    field,
+    // fieldState: { invalid, isTouched, isDirty },
+    // formState: { touchedFields, dirtyFields },
+  } = useController({
+    name,
+    control,
+    defaultValue: false,
+  });
+
+  return (
+    <>
+      {label && (
+        <label htmlFor={name} className="block text-sm font-medium text-gray-700 mb-1">
+          {label}
+        </label>
+      )}
+      <input
+        type="checkbox"
+        id={name}
+        onChange={field.onChange} // send value to hook form
+        onBlur={field.onBlur} // notify when input is touched/blur
+        checked={field.value}
+        name={name} // send down the checkbox name
+        className="shadow-sm focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
+        {...props}
+      />
+    </>
+  );
+};
+
+type CheckboxProps = {
+  control: any;
+  name: string;
+  id?: string;
+  label?: string;
+  className?: string;
+};
diff --git a/src/components/Form/Checkbox/index.ts b/src/components/Form/Checkbox/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f59e4fbb54cf280c10a3e02778f41c4cb09a43a
--- /dev/null
+++ b/src/components/Form/Checkbox/index.ts
@@ -0,0 +1 @@
+export { Checkbox } from './Checkbox';
diff --git a/src/components/Form/CodeEditor/CodeEditor.tsx b/src/components/Form/CodeEditor/CodeEditor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b395f8bd50fb790838349f8bbf1d159f0c3a63b3
--- /dev/null
+++ b/src/components/Form/CodeEditor/CodeEditor.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { highlight, languages } from 'prismjs';
+import { useController } from 'react-hook-form';
+import Editor from 'react-simple-code-editor';
+
+/* eslint-disable react/react-in-jsx-scope */
+export const CodeEditor = ({ control, name, required, disabled = false }: CodeEditorProps) => {
+  const {
+    field,
+    // fieldState: { invalid, isTouched, isDirty },
+    // formState: { touchedFields, dirtyFields },
+  } = useController({
+    name,
+    control,
+    rules: { required },
+    defaultValue: '',
+  });
+
+  return (
+    <>
+      <Editor
+        value={field.value}
+        onValueChange={field.onChange}
+        highlight={(value) => highlight(value, languages.js, 'yaml')}
+        preClassName="font-mono whitespace-normal font-light"
+        textareaClassName="font-mono overflow-auto font-light"
+        className="font-mono text-sm font-light"
+        disabled={disabled}
+      />
+    </>
+  );
+};
+
+type CodeEditorProps = {
+  control: any;
+  name: string;
+  required?: boolean;
+  disabled?: boolean;
+};
diff --git a/src/components/Form/CodeEditor/index.ts b/src/components/Form/CodeEditor/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9f6648589e9fda8625b1ae948da0337d71bb8efa
--- /dev/null
+++ b/src/components/Form/CodeEditor/index.ts
@@ -0,0 +1 @@
+export { CodeEditor } from './CodeEditor';
diff --git a/src/components/Form/Switch/Switch.tsx b/src/components/Form/Switch/Switch.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d1596daa9ab545da3c4e35a085adb82883343fc2
--- /dev/null
+++ b/src/components/Form/Switch/Switch.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { useController } from 'react-hook-form';
+import { Switch as HeadlessSwitch } from '@headlessui/react';
+
+function classNames(...classes: any) {
+  return classes.filter(Boolean).join(' ');
+}
+/* eslint-disable react/react-in-jsx-scope */
+export const Switch = ({ control, name, label }: SwitchProps) => {
+  const {
+    field,
+    // fieldState: { invalid, isTouched, isDirty },
+    // formState: { touchedFields, dirtyFields },
+  } = useController({
+    name,
+    control,
+    defaultValue: '',
+  });
+
+  return (
+    <>
+      {label && (
+        <label htmlFor={name} className="block text-sm font-medium text-gray-700 mb-1">
+          {label}
+        </label>
+      )}
+      <HeadlessSwitch
+        checked={field.value}
+        onChange={field.onChange}
+        className={classNames(
+          field.value ? 'bg-primary-600' : 'bg-gray-200',
+          'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none',
+        )}
+      >
+        <span
+          aria-hidden="true"
+          className={classNames(
+            field.value ? 'translate-x-5' : 'translate-x-0',
+            'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
+          )}
+        />
+      </HeadlessSwitch>
+    </>
+  );
+};
+
+type SwitchProps = {
+  control: any;
+  name: string;
+  label?: string;
+};
diff --git a/src/components/Form/Switch/index.ts b/src/components/Form/Switch/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cee89a18b17be637fed0ade07fadb4231d153757
--- /dev/null
+++ b/src/components/Form/Switch/index.ts
@@ -0,0 +1 @@
+export { Switch } from './Switch';
diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts
index 2cc92984681dc27fabc68665ce8ab44a97834e7e..efb50653d1c3d164af8670013c7a3c1567c81a3f 100644
--- a/src/components/Form/index.ts
+++ b/src/components/Form/index.ts
@@ -1,3 +1,6 @@
 export { Input } from './Input';
 export { Select } from './Select';
+export { Switch } from './Switch';
+export { CodeEditor } from './CodeEditor';
 export { TextArea } from './TextArea';
+export { Checkbox } from './Checkbox';
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 97a24eda56729ac74454a0b75d12385be3c0d293..0c783e1597290ed72404d259fe90b04d0ac4f62f 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -14,6 +14,7 @@ const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessi
 const navigation = [
   { name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
   { name: 'Users', to: '/users', requiresAdmin: true },
+  { name: 'Apps', to: '/apps', requiresAdmin: true },
 ];
 
 function classNames(...classes: any[]) {
diff --git a/src/components/Modal/Modal/Modal.tsx b/src/components/Modal/Modal/Modal.tsx
index e61aaee56622b8773885a9fb40af927c4e47c471..d64acfecb2c30877b7187498af02dbd616307510 100644
--- a/src/components/Modal/Modal/Modal.tsx
+++ b/src/components/Modal/Modal/Modal.tsx
@@ -7,9 +7,11 @@ export const Modal: React.FC<ModalProps> = ({
   open,
   onClose,
   onSave,
+  saveButtonTitle = 'Save Changes',
   children,
   title = '',
   useCancelButton = false,
+  cancelButtonTitle = 'Cancel',
   isLoading = false,
   leftActions = <></>,
   saveButtonDisabled = false,
@@ -94,7 +96,7 @@ export const Modal: React.FC<ModalProps> = ({
                       ref={saveButtonRef}
                       disabled={saveButtonDisabled}
                     >
-                      Save Changes
+                      {saveButtonTitle}
                     </button>
                     {useCancelButton && (
                       <button
@@ -103,7 +105,7 @@ export const Modal: React.FC<ModalProps> = ({
                         onClick={onClose}
                         ref={cancelButtonRef}
                       >
-                        Cancel
+                        {cancelButtonTitle}
                       </button>
                     )}
                   </div>
diff --git a/src/components/Modal/Modal/types.ts b/src/components/Modal/Modal/types.ts
index e679e696d4687ed1d1d2898721fcc82de22dedd3..52cf784b6f07871aa74f3a9c28a4b9c86e2d0a91 100644
--- a/src/components/Modal/Modal/types.ts
+++ b/src/components/Modal/Modal/types.ts
@@ -5,7 +5,9 @@ export type ModalProps = {
   onClose: () => void;
   title?: string;
   onSave?: () => void;
+  saveButtonTitle?: string;
   useCancelButton?: boolean;
+  cancelButtonTitle?: string;
   isLoading?: boolean;
   leftActions?: React.ReactNode;
   saveButtonDisabled?: boolean;
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index 157b72896aea57e98df4683667a6346fa7dcbe1c..95f22567a3a0760f239c38ab0f8a03b6f1e8840c 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -166,7 +166,7 @@ export const Table = <T extends Record<string, unknown>>({
                       />
                     </svg>
                   </div>
-                  <p className="text-sm text-primary-600 mt-2">Loading users</p>
+                  <p className="text-sm text-primary-600 mt-2">Loading...</p>
                 </div>
               </td>
             </tr>
diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx
index 850dc1779b6bf76805d6e098822f586976aadaf6..2c8dea97ab953fc098ae5daf96a9cba06ebe916c 100644
--- a/src/components/Tabs/Tabs.tsx
+++ b/src/components/Tabs/Tabs.tsx
@@ -2,11 +2,14 @@ import React, { useState } from 'react';
 import { TabPanel } from './TabPanel';
 import { TabsProps } from './types';
 
-export const Tabs = ({ tabs }: TabsProps) => {
+export const Tabs = ({ tabs, onTabClick }: TabsProps) => {
   const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
 
   const handleTabPress = (index: number) => () => {
     setActiveTabIndex(index);
+    if (onTabClick) {
+      onTabClick(index);
+    }
   };
 
   function classNames(...classes: any) {
@@ -23,7 +26,6 @@ export const Tabs = ({ tabs }: TabsProps) => {
           id="tabs"
           name="tabs"
           className="block w-full focus:ring-primary-500 focus:border-primary-500 border-gray-300 rounded-md"
-          //   defaultValue={tabs ? tabs.find((tab) => tab.current).name : undefined}
         >
           {tabs.map((tab) => (
             <option key={tab.name}>{tab.name}</option>
diff --git a/src/components/Tabs/types.ts b/src/components/Tabs/types.ts
index 342266acd220d0ea74eb7af77183e8df4ab466f2..00584fd15fc4710b35ad50251954b8e4f08624db 100644
--- a/src/components/Tabs/types.ts
+++ b/src/components/Tabs/types.ts
@@ -6,6 +6,7 @@ type Tab = {
 
 export interface TabsProps {
   tabs: Tab[];
+  onTabClick?: (index: number) => void;
 }
 
 export interface TabPanelProps {
diff --git a/src/components/UserModal/consts.ts b/src/components/UserModal/consts.ts
index f1566526e4ea6ef338681db255c68873128b1321..d17985ae10ef9db92f9997c4529c34b93c735044 100644
--- a/src/components/UserModal/consts.ts
+++ b/src/components/UserModal/consts.ts
@@ -5,21 +5,31 @@ export const appAccessList = [
     name: 'wekan',
     image: '/assets/wekan.svg',
     label: 'Wekan',
+    documentationUrl: 'https://github.com/wekan/wekan/wiki',
   },
   {
     name: 'wordpress',
     image: '/assets/wordpress.svg',
     label: 'Wordpress',
+    documentationUrl: 'https://wordpress.org/support/',
   },
   {
     name: 'nextcloud',
     image: '/assets/nextcloud.svg',
     label: 'Nextcloud',
+    documentationUrl: 'https://docs.nextcloud.com/server/latest/user_manual/en/',
   },
   {
     name: 'zulip',
     image: '/assets/zulip.svg',
     label: 'Zulip',
+    documentationUrl: 'https://docs.zulip.com/help/',
+  },
+  {
+    name: 'monitoring',
+    image: '/assets/monitoring.svg',
+    label: 'Monitoring',
+    documentationUrl: 'https://grafana.com/docs/',
   },
 ];
 
diff --git a/src/modules/apps/AppSingle.tsx b/src/modules/apps/AppSingle.tsx
index fc994e3838efce030b5e1fbe1f20f98e432787e9..92b84f5e488d5aa4a5db6c08c5b683927964d0d8 100644
--- a/src/modules/apps/AppSingle.tsx
+++ b/src/modules/apps/AppSingle.tsx
@@ -1,96 +1,173 @@
-import React from 'react';
-import { ChevronRightIcon } from '@heroicons/react/solid';
+/**
+ * This page shows information about a single application. It contains several
+ * configuration options (that are not implemented in the back-end yet) such as:
+ *
+ * 1. Toggling auto-updates
+ * 2. Advanced configuration by overwriting helm values
+ * 3. Deleting the application
+ *
+ * This page is only available for Admin users.
+ */
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useForm, useWatch } from 'react-hook-form';
+import _ from 'lodash';
 import { XCircleIcon } from '@heroicons/react/outline';
-import { Tabs, Banner } from 'src/components';
-import { Link } from 'react-router-dom';
+import { DisableAppForm, useApps } from 'src/services/apps';
+import { Modal, Tabs } from 'src/components';
+import { Checkbox } from 'src/components/Form';
+import { appAccessList } from 'src/components/UserModal/consts';
 import { AdvancedTab, GeneralTab } from './components';
 
-const pages = [
-  { name: 'Apps', to: '/apps', current: true },
-  { name: 'Nextcloud', to: '', current: false },
-];
+export const AppSingle: React.FC = () => {
+  const [disableAppModal, setDisableAppModal] = useState(false);
+  const [removeAppData, setRemoveAppData] = useState(false);
+  const params = useParams();
+  const appSlug = params.slug;
+  const { app, loadApp, disableApp, clearSelectedApp } = useApps();
+  const navigate = useNavigate();
 
-const tabs = [
-  { name: 'General', component: <GeneralTab /> },
-  { name: 'Advanced Configuration', component: <AdvancedTab /> },
-];
+  const initialDisableData = { slug: appSlug, removeAppData: false };
 
-export const AppSingle: React.FC = () => {
-  return (
-    <>
-      <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 flex-grow">
-        <nav className="flex" aria-label="Breadcrumb">
-          <ol className="flex items-center space-x-4">
-            <li>
-              <div className="flex items-center">
-                <Link to="/dashboard" className="text-sm font-medium text-gray-500 hover:text-gray-700">
-                  <span>Dashboard</span>
-                </Link>
-              </div>
-            </li>
-            {pages.map((page) => (
-              <li key={page.name}>
-                <div className="flex items-center">
-                  <ChevronRightIcon className="flex-shrink-0 h-5 w-5 text-gray-400" aria-hidden="true" />
-                  <Link
-                    to={page.to}
-                    className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
-                    aria-current={page.current ? 'page' : undefined}
-                  >
-                    {page.name}
-                  </Link>
-                </div>
-              </li>
-            ))}
-          </ol>
-        </nav>
-      </div>
+  const { control, reset, handleSubmit } = useForm<DisableAppForm>({
+    defaultValues: initialDisableData,
+  });
 
-      <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden">
-        <Banner title="Managing single app instances coming soon." titleSm="Comming soon!" />
-      </div>
+  const removeAppDataWatch = useWatch({
+    control,
+    name: 'removeAppData',
+  });
 
-      <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden opacity-40 cursor-default pointer-events-none select-none">
-        <div className="bg-white overflow-hidden shadow rounded-sm mb-5">
-          <div className="px-4 py-5 sm:p-6 flex justify-between items-center">
-            <div className="mr-4 flex items-center">
-              <img
-                className="h-24 w-24 rounded-md overflow-hidden mr-4"
-                src="./../assets/nextcloud.svg"
-                alt="Nextcloud"
-              />
+  useEffect(() => {
+    setRemoveAppData(removeAppDataWatch);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [removeAppDataWatch]);
 
-              <div>
-                <h2 className="text-2xl leading-8 font-bold">Nextcloud</h2>
-                <div className="text-sm leading-5 font-medium text-gray-500">Installed on August 25, 2020</div>
-              </div>
-            </div>
+  useEffect(() => {
+    if (appSlug) {
+      loadApp(appSlug);
+    }
 
-            <div className="flex flex-col">
-              <button
-                type="button"
-                className="mb-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-yellow-900 bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
-              >
-                <XCircleIcon className="-ml-0.5 mr-2 h-4 w-4 text-yellow-900" aria-hidden="true" />
-                Disable App
-              </button>
-
-              <button
-                type="button"
-                className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
-              >
-                View Documentation
-              </button>
-            </div>
+    return () => {
+      clearSelectedApp();
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [appSlug]);
+
+  if (!app) {
+    return null;
+  }
+
+  const appImageSrc = _.find(appAccessList, { name: appSlug })?.image;
+  const appDocumentationUrl = _.find(appAccessList, { name: appSlug })?.documentationUrl;
+
+  const openDocumentationInNewTab = () => {
+    window.open(appDocumentationUrl, '_blank', 'noopener,noreferrer');
+  };
+
+  const tabs = [
+    {
+      name: 'General',
+      component: <GeneralTab />,
+    },
+    { name: 'Advanced Configuration', component: <AdvancedTab /> },
+  ];
+
+  const onDisableApp = async () => {
+    try {
+      await handleSubmit((data) => disableApp(data))();
+    } catch (e: any) {
+      // Continue
+    }
+    setDisableAppModal(false);
+    clearSelectedApp();
+    navigate('/apps');
+  };
+
+  const handleCloseDisableModal = () => {
+    reset(initialDisableData);
+    setDisableAppModal(false);
+  };
+
+  return (
+    <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden lg:flex lg:flex-row">
+      <div
+        className="block bg-white overflow-hidden shadow rounded-sm basis-4/12 mx-auto sm:px-6 lg:px-8 overflow-hidden block lg:flex-none"
+        style={{ height: 'fit-content' }}
+      >
+        <div className="px-4 py-5 sm:p-6 flex flex-col">
+          <img className="h-24 w-24 rounded-md overflow-hidden mr-4 mb-3" src={appImageSrc} alt={app.name} />
+          <div className="mb-3">
+            <h2 className="text-2xl leading-8 font-bold">{app.name}</h2>
+            <div className="text-sm leading-5 font-medium text-gray-500">Installed on August 25, 2020</div>
           </div>
+          <button
+            type="button"
+            className="mb-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-yellow-900 bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
+            onClick={() => setDisableAppModal(true)}
+          >
+            <XCircleIcon className="-ml-0.5 mr-2 h-4 w-4 text-yellow-900" aria-hidden="true" />
+            Disable App
+          </button>
+          <button
+            type="button"
+            className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
+            onClick={openDocumentationInNewTab}
+          >
+            View manual
+          </button>
         </div>
+      </div>
 
-        <div className="bg-white shadow rounded-sm">
+      <div className="mx-auto sm:mt-8 lg:mt-0 lg:px-8 overflow-hidden block">
+        <div className="bg-white sm:px-6 shadow rounded-sm basis-8/12">
           <div className="px-4 py-5 sm:p-6">
             <Tabs tabs={tabs} />
           </div>
         </div>
       </div>
-    </>
+
+      {disableAppModal && (
+        <Modal
+          onClose={handleCloseDisableModal}
+          open={disableAppModal}
+          onSave={onDisableApp}
+          saveButtonTitle={removeAppData ? `Yes, delete  and it's data` : 'Yes, delete'}
+          cancelButtonTitle="No, cancel"
+          useCancelButton
+        >
+          <div className="bg-white px-4">
+            <div className="space-y-10 divide-y divide-gray-200">
+              <div>
+                <div>
+                  <h3 className="text-lg leading-6 font-medium text-gray-900">Disable app</h3>
+                </div>
+                <div className="px-4 py-5 sm:p-6">
+                  Are you sure you want to disable {app.name}? The app will get uninstalled and none of your users will
+                  be able to access the app.
+                </div>
+                <fieldset className="px-4 py-5 sm:p-6">
+                  <div className="relative flex items-start">
+                    <div className="flex items-center h-5">
+                      <Checkbox control={control} name="removeAppData" id="removeAppData" />
+                    </div>
+                    <div className="ml-3 text-sm">
+                      <label htmlFor="removeAppData" className="font-medium text-gray-700">
+                        Remove app data
+                      </label>
+                      <p id="removeAppData-description" className="text-gray-500">
+                        {removeAppData
+                          ? `The app's data will be removed. After this operation is done you will not be able to access the app, nor the app data. If you re-install the app, it will have none of the data it had before.`
+                          : `The app's data does not get removed. If you install the app again, you will be able to access the data again.`}
+                      </p>
+                    </div>
+                  </div>
+                </fieldset>
+              </div>
+            </div>
+          </div>
+        </Modal>
+      )}
+    </div>
   );
 };
diff --git a/src/modules/apps/Apps.tsx b/src/modules/apps/Apps.tsx
index 48d244fcf20b84c00c81da20719ed5171b28627e..ba83667f65c3475aec4e5f62f0541f6f6800d91e 100644
--- a/src/modules/apps/Apps.tsx
+++ b/src/modules/apps/Apps.tsx
@@ -1,167 +1,129 @@
 /* eslint-disable react-hooks/exhaustive-deps */
-import React, { useState, useCallback, useMemo } from 'react';
-import { ChevronRightIcon, SearchIcon, PlusIcon } from '@heroicons/react/solid';
-import { CogIcon, TrashIcon } from '@heroicons/react/outline';
-import { ConfirmationModal } from 'src/components/Modal';
-import { Table, Banner } from 'src/components';
-import { Link } from 'react-router-dom';
-
-const pages = [{ name: 'Apps', href: '#', current: true }];
+/**
+ * This page shows all the applications and their status in a table.
+ *
+ * This page is only available for Admin users.
+ */
+import React, { useState, useCallback, useMemo, useEffect } from 'react';
+// import { useNavigate } from 'react-router';
+import { SearchIcon } from '@heroicons/react/solid';
+import { showToast, ToastType } from 'src/common/util/show-toast';
+import _, { debounce } from 'lodash';
+import { Table } from 'src/components';
+import { App, AppStatusEnum, useApps } from 'src/services/apps';
+// import { AppInstallModal } from './components';
+import { getConstForStatus } from './consts';
 
 export const Apps: React.FC = () => {
-  const [selectedRowsIds, setSelectedRowsIds] = useState({});
-  const [deleteModal, setDeleteModal] = useState(false);
-  const [search, setSearch] = useState('');
+  // If you want to enable the App Install button again, uncomment this:
+  // const [installModalOpen, setInstallModalOpen] = useState(false);
+  // const [appSlug, setAppSlug] = useState(null);
 
-  const deleteModalOpen = () => setDeleteModal(true);
-  const deleteModalClose = () => setDeleteModal(false);
+  const [search, setSearch] = useState('');
+  const { apps, appTableLoading, loadApps } = useApps();
 
   const handleSearch = useCallback((event: any) => {
     setSearch(event.target.value);
   }, []);
 
-  const data: any[] = useMemo(
-    () => [
-      {
-        id: 1,
-        name: 'Nextcloud',
-        status: 'Active for everyone',
-        assetSrc: './assets/nextcloud.svg',
-      },
-      {
-        id: 2,
-        name: 'Wekan',
-        status: 'Active for everyone',
-        assetSrc: './assets/wekan.svg',
-      },
-      {
-        id: 3,
-        name: 'Rocketchat',
-        status: 'Active for everyone',
-        assetSrc: './assets/rocketchat.svg',
-      },
-      {
-        id: 4,
-        name: 'Wordpress',
-        status: 'Active for everyone',
-        assetSrc: './assets/wordpress.svg',
-      },
-    ],
-    [],
-  );
+  const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
+
+  useEffect(() => {
+    loadApps();
+
+    return () => {
+      debouncedSearch.cancel();
+    };
+  }, []);
 
   const filterSearch = useMemo(() => {
-    return data.filter((item: any) => item.name?.toLowerCase().includes(search.toLowerCase()));
-  }, [search]);
+    return _.filter(apps, (item) => item.name?.toLowerCase().includes(search.toLowerCase()));
+  }, [apps, search]);
 
-  const columns: any = useMemo(
-    () => [
-      {
-        Header: 'Name',
-        accessor: 'name',
-        Cell: (e: any) => {
-          return (
-            <div className="flex items-center">
-              <div className="flex-shrink-0 h-10 w-10">
-                <img className="h-10 w-10 rounded-md overflow-hidden" src={e.cell.row.original.assetSrc} alt="" />
-              </div>
-              <div className="ml-4">
-                <div className="text-sm font-medium text-gray-900">{e.cell.row.original.name}</div>
-              </div>
+  const columns: any = [
+    {
+      Header: 'Name',
+      accessor: 'name',
+      Cell: (e: any) => {
+        const app = e.cell.row.original as App;
+        return (
+          <div className="flex items-center">
+            <div className="flex-shrink-0 h-10 w-10">
+              <img className="h-10 w-10 rounded-md overflow-hidden" src={app.assetSrc} alt={app.name} />
             </div>
-          );
-        },
-        width: 'auto',
-      },
-      {
-        Header: 'Status',
-        accessor: 'status',
-        Cell: (e: any) => {
-          return (
-            <div className="flex items-center">
-              <div className="flex-shrink-0 h-4 w-4 rounded-full bg-green-600" />
-              <div className="ml-2 text-sm text-green-600">{e.cell.row.original.status}</div>
+            <div className="ml-4">
+              <div className="text-sm font-medium text-gray-900">{app.name}</div>
             </div>
-          );
-        },
-        width: 'auto',
+          </div>
+        );
       },
-      {
-        Header: ' ',
-        Cell: () => {
-          return (
-            <div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
-              <button
-                onClick={() => {}}
-                type="button"
-                className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+      width: 'auto',
+    },
+    {
+      Header: 'Status',
+      accessor: 'status',
+      Cell: (e: any) => {
+        const status = e.cell.row.original.status as AppStatusEnum;
+        return (
+          <div className="flex items-center">
+            <div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} />
+            {status === AppStatusEnum.Installing ? (
+              <div
+                className={`ml-2 cursor-pointer text-sm text-${getConstForStatus(status, 'colorClass')}`}
+                onClick={() => showToast('Installing an app can take up to 10 minutes.', ToastType.Success)}
               >
-                <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-                Configure
-              </button>
-            </div>
-          );
-        },
-        width: 'auto',
+                {status}
+              </div>
+            ) : (
+              <div className={`ml-2 text-sm text-${getConstForStatus(status, 'colorClass')}`}>{status}</div>
+            )}
+          </div>
+        );
       },
-    ],
-    [],
-  );
-
-  const selectedRows = useCallback((rows: Record<string, boolean>) => {
-    setSelectedRowsIds(rows);
-  }, []);
+      width: 'auto',
+    },
+    // If you want to enable the App Install button again, uncomment this:
+    //
+    // We need to implement installation and configuration in the back-end to be
+    // able to use those buttons.
+    // {
+    //   Header: ' ',
+    //   Cell: (e: any) => {
+    //     const navigate = useNavigate();
+    //     const appStatus = e.cell.row.original.status as AppStatusEnum;
+    //     if (appStatus === AppStatusEnum.Installing) {
+    //       return null;
+    //     }
+    //     const { slug } = e.cell.row.original;
+    //     let buttonFunction = () => navigate(`/apps/${slug}`);
+    //     if (appStatus === AppStatusEnum.NotInstalled) {
+    //       buttonFunction = () => {
+    //         setAppSlug(slug);
+    //         setInstallModalOpen(true);
+    //       };
+    //     }
+    //     return (
+    //       <div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
+    //         <button
+    //           onClick={buttonFunction}
+    //           type="button"
+    //           className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+    //         >
+    //           {getConstForStatus(appStatus, 'buttonIcon')}
+    //           {getConstForStatus(appStatus, 'buttonTitle')}
+    //         </button>
+    //       </div>
+    //     );
+    //   },
+    //   width: 'auto',
+    // },
+  ];
 
   return (
-    <>
-      <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 flex-grow">
-        <nav className="flex" aria-label="Breadcrumb">
-          <ol className="flex items-center space-x-4">
-            <li>
-              <div className="flex items-center">
-                <Link to="/dashboard" className="text-sm font-medium text-gray-500 hover:text-gray-700">
-                  <span>Dashboard</span>
-                </Link>
-              </div>
-            </li>
-            {pages.map((page) => (
-              <li key={page.name}>
-                <div className="flex items-center">
-                  <ChevronRightIcon className="flex-shrink-0 h-5 w-5 text-gray-400" aria-hidden="true" />
-                  <a
-                    href={page.href}
-                    className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
-                    aria-current={page.current ? 'page' : undefined}
-                  >
-                    {page.name}
-                  </a>
-                </div>
-              </li>
-            ))}
-          </ol>
-        </nav>
-      </div>
-
-      <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden">
-        <Banner
-          title="Your app instances management will be here soon, in the meantime, feel free to explore."
-          titleSm="Comming soon!"
-        />
-      </div>
-
-      <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 flex-grow">
-        <div className="pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
+    <div className="relative">
+      <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
+        <div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
           <h1 className="text-3xl leading-6 font-bold text-gray-900">Apps</h1>
-          <div className="mt-3 sm:mt-0 sm:ml-4">
-            <button
-              disabled
-              type="button"
-              className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
-            >
-              <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-              Add new app
-            </button>
-          </div>
         </div>
 
         <div className="flex justify-between w-100 my-3 items-center">
@@ -187,36 +149,30 @@ export const Apps: React.FC = () => {
               </div>
             </div>
           </div>
-
-          {selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
-            <button
-              onClick={deleteModalOpen}
-              type="button"
-              className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
-            >
-              <TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-              Delete
-            </button>
-          )}
         </div>
 
         <div className="flex flex-col">
           <div className="-my-2 sm:-mx-6 lg:-mx-8">
             <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
               <div className="shadow border-b border-gray-200 sm:rounded-lg">
-                <Table data={filterSearch} columns={columns} getSelectedRowIds={selectedRows} selectable />
+                <Table
+                  data={_.filter(filterSearch, (app) => app.slug !== 'dashboard') as any}
+                  columns={columns}
+                  loading={appTableLoading}
+                />
               </div>
             </div>
           </div>
         </div>
-
-        <ConfirmationModal
-          open={deleteModal}
-          onClose={deleteModalClose}
-          title="Delete service"
-          body="Are you sure you want to delete this service? All of your data will be permanently removed. This action cannot be undone."
-        />
       </div>
-    </>
+
+      {
+        // If you want to enable the App Install button again, uncomment this:
+        //
+        // installModalOpen && (
+        //   <AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} />
+        // )
+      }
+    </div>
   );
 };
diff --git a/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx b/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx
index a3b7c34dfd5f75d9208ea71ea6532a768e43c53c..ec3e9480606a0eae794e49a256a9341949a92402 100644
--- a/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx
+++ b/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx
@@ -1,20 +1,50 @@
-import React, { Fragment } from 'react';
+import React from 'react';
+import _ from 'lodash';
 import Editor from 'react-simple-code-editor';
-import { Menu, Transition } from '@headlessui/react';
-import { ChevronDownIcon } from '@heroicons/react/solid';
+// import { Menu, Transition } from '@headlessui/react';
+// import { ChevronDownIcon } from '@heroicons/react/solid';
+import yaml from 'js-yaml';
 import { highlight, languages } from 'prismjs';
 import 'prismjs/components/prism-clike';
 import 'prismjs/components/prism-yaml';
 import 'prismjs/themes/prism.css';
+import { showToast, ToastType } from 'src/common/util/show-toast';
+import { useApps } from 'src/services/apps';
 import { initialEditorYaml } from '../../consts';
-import { Secrets } from './components';
-
-function classNames(...classes: any) {
-  return classes.filter(Boolean).join(' ');
-}
 
 export const AdvancedTab = () => {
   const [code, setCode] = React.useState(initialEditorYaml);
+  const { app, editApp } = useApps();
+
+  const resetCode = () => {
+    setCode(initialEditorYaml);
+    showToast('Code was reset.');
+  };
+
+  const isConfigurationValid = () => {
+    try {
+      yaml.load(code);
+      return true;
+    } catch (e: any) {
+      return false;
+    }
+  };
+
+  const verifyCode = () => {
+    if (isConfigurationValid()) {
+      showToast('Configuration is valid.', ToastType.Success);
+    } else {
+      showToast('Configuration is not valid! Please fix configuration issues and try again.', ToastType.Error);
+    }
+  };
+
+  const saveChanges = () => {
+    if (isConfigurationValid()) {
+      editApp({ ...app, configuration: code });
+      return;
+    }
+    showToast('Configuration is not valid! Please fix configuration issues and try again.', ToastType.Error);
+  };
 
   return (
     <>
@@ -22,9 +52,24 @@ export const AdvancedTab = () => {
         <h1 className="text-2xl leading-6 font-medium text-gray-900">Configuration</h1>
       </div>
       <div className="grid grid-flow-col grid-cols-2 gap-8">
-        <div>
-          <div>
-            <div className="px-4 h-16 sm:px-6 bg-gray-200 flex justify-between items-center rounded-t-lg">
+        <div className="bg-gray-100 overflow-hidden rounded-lg">
+          <div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center">
+            <span className="text-gray-600 text-lg leading-6 font-medium">Current Configuration</span>
+          </div>
+          <div className="px-4 py-5 sm:p-6 overflow-x-auto">
+            <Editor
+              value={initialEditorYaml}
+              onValueChange={_.noop}
+              highlight={(value) => highlight(value, languages.js, 'yaml')}
+              preClassName="font-mono text-sm font-light"
+              textareaClassName="font-mono overflow-auto font-light"
+              className="font-mono text-sm font-light"
+              disabled
+            />
+          </div>
+        </div>
+        <div className="overflow-hidden rounded-lg">
+          {/* <div className="px-4 h-16 sm:px-6 bg-gray-200 flex justify-between items-center rounded-t-lg">
               <span className="text-gray-600 text-lg leading-6 font-medium">Edit Configuration</span>
 
               <Menu as="div" className="relative inline-block text-left">
@@ -89,62 +134,34 @@ export const AdvancedTab = () => {
                   </Menu.Items>
                 </Transition>
               </Menu>
-            </div>
-            <div className="px-4 py-5 sm:p-6 border border-t-0 border-gray-200">
-              <Editor
-                value={code}
-                onValueChange={(value) => setCode(value)}
-                highlight={(value) => highlight(value, languages.js, 'yaml')}
-                preClassName="font-mono whitespace-normal font-light"
-                textareaClassName="font-mono overflow-auto font-light"
-                className="font-mono text-sm font-light"
-              />
-            </div>
+            </div> */}
+          <div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center">
+            <span className="text-gray-600 text-lg leading-6 font-medium">Edit Configuration</span>
           </div>
-        </div>
-        <div>
-          <div className="bg-gray-100 overflow-hidden rounded-lg">
-            <div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center">
-              <span className="text-gray-600 text-lg leading-6 font-medium">Current Configuration</span>
-            </div>
-            <div className="px-4 py-5 sm:p-6 overflow-x-auto">
-              <pre className="font-mono text-sm font-light">
-                {`luck: except
-natural: still
-near: though
-search:
-  - feature
-  - - 1980732354.689713
-    - hour
-    - butter:
-        ordinary: 995901949.8974948
-        teeth: true
-        whole:
-          - -952367353
-          - - talk: -1773961379
-              temperature: false
-              oxygen: true
-              laugh:
-                flag:
-                  in: 2144751662
-                  hospital: -1544066384.1973226
-                  law: congress
-                  great: stomach`}
-              </pre>
-            </div>
+          <div className="px-4 py-5 sm:p-6 border border-t-0 border-gray-200">
+            <Editor
+              value={code}
+              onValueChange={(value) => setCode(value)}
+              highlight={(value) => highlight(value, languages.js, 'yaml')}
+              preClassName="font-mono whitespace-normal font-light"
+              textareaClassName="font-mono overflow-auto font-light"
+              className="font-mono text-sm font-light"
+            />
           </div>
         </div>
       </div>
       <div className="flex justify-end mt-10">
         <button
           type="button"
+          onClick={resetCode}
           className="mr-3 inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
         >
-          Cancel
+          Reset
         </button>
 
         <button
           type="button"
+          onClick={verifyCode}
           className="mr-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
         >
           Verify
@@ -152,13 +169,12 @@ search:
 
         <button
           type="button"
+          onClick={saveChanges}
           className="inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
         >
           Save changes
         </button>
       </div>
-
-      <Secrets />
     </>
   );
 };
diff --git a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..14e0b1a11f601e23d885149ae18d762ccf48c330
--- /dev/null
+++ b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx
@@ -0,0 +1,81 @@
+import React, { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import _ from 'lodash';
+import { App, useApps } from 'src/services/apps';
+import { Modal } from 'src/components';
+import { Input } from 'src/components/Form';
+import { AppInstallModalProps } from './types';
+import { initialAppForm, initialCode } from './consts';
+
+export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps) => {
+  const [appName, setAppName] = useState('');
+  const { app, appLoading, installApp, loadApp, clearSelectedApp } = useApps();
+
+  const { control, reset, handleSubmit } = useForm<App>({
+    defaultValues: initialAppForm,
+  });
+
+  useEffect(() => {
+    if (appSlug) {
+      loadApp(appSlug);
+    }
+
+    return () => {
+      reset(initialAppForm);
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [appSlug, open]);
+
+  useEffect(() => {
+    if (!_.isEmpty(app)) {
+      setAppName(app.name);
+      reset({ url: app.url, configuration: initialCode, slug: appSlug ?? '' });
+    }
+
+    return () => {
+      reset({ url: initialAppForm.url, configuration: initialCode, slug: appSlug ?? '' });
+    };
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [app, reset, open]);
+
+  const handleClose = () => {
+    clearSelectedApp();
+    reset();
+    onClose();
+  };
+
+  const handleSave = async () => {
+    try {
+      await handleSubmit((data) => installApp(data))();
+    } catch (e: any) {
+      // Continue
+    }
+    handleClose();
+  };
+
+  const handleKeyPress = (e: any) => {
+    if (e.key === 'Enter' || e.key === 'NumpadEnter') {
+      handleSave();
+    }
+  };
+
+  return (
+    <Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={appLoading} useCancelButton>
+      <div className="bg-white px-4">
+        <div className="space-y-10 divide-y divide-gray-200">
+          <div>
+            <div>
+              <h3 className="text-lg leading-6 font-medium text-gray-900">Install app {appName}</h3>
+            </div>
+
+            <div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
+              <div className="sm:col-span-3">
+                <Input control={control} name="url" label="URL" onKeyPress={handleKeyPress} required={false} />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </Modal>
+  );
+};
diff --git a/src/modules/apps/consts.ts b/src/modules/apps/components/AppInstallModal/consts.ts
similarity index 77%
rename from src/modules/apps/consts.ts
rename to src/modules/apps/components/AppInstallModal/consts.ts
index 0a85e78e42e1e1f5ca377a5359f64ba6c6fa4e2c..22a30bcef0eb0be688147dcdfaf9f6ad6e7d1b47 100644
--- a/src/modules/apps/consts.ts
+++ b/src/modules/apps/components/AppInstallModal/consts.ts
@@ -1,5 +1,6 @@
-export const initialEditorYaml = () => {
-  return `luck: except
+import { App } from 'src/services/apps';
+
+export const initialCode = `luck: except
 natural: still
 near: though
 search:
@@ -20,4 +21,7 @@ search:
                   hospital: -1544066384.1973226
                   law: congress
                   great: stomach`;
-};
+
+export const initialAppForm = {
+  url: '',
+} as App;
diff --git a/src/modules/apps/components/AppInstallModal/index.ts b/src/modules/apps/components/AppInstallModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0796470d67a1eb1aaf148ab86b42a1057341472e
--- /dev/null
+++ b/src/modules/apps/components/AppInstallModal/index.ts
@@ -0,0 +1 @@
+export { AppInstallModal } from './AppInstallModal';
diff --git a/src/modules/apps/components/AppInstallModal/types.ts b/src/modules/apps/components/AppInstallModal/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f10e7e44be882761e584dbdfe002986469d0329
--- /dev/null
+++ b/src/modules/apps/components/AppInstallModal/types.ts
@@ -0,0 +1,5 @@
+export type AppInstallModalProps = {
+  open: boolean;
+  onClose: () => void;
+  appSlug: string | null;
+};
diff --git a/src/modules/apps/components/GeneralTab/GeneralTab.tsx b/src/modules/apps/components/GeneralTab/GeneralTab.tsx
index 1e5239be7fff6ea5df4fcecc1037ac4ec6a323b1..9b7054ddf79e3eb1ac9a7ab77dd731db8e79efc4 100644
--- a/src/modules/apps/components/GeneralTab/GeneralTab.tsx
+++ b/src/modules/apps/components/GeneralTab/GeneralTab.tsx
@@ -1,161 +1,59 @@
-import React, { useState } from 'react';
-import { RadioGroup } from '@headlessui/react';
+import _ from 'lodash';
+import React, { useEffect } from 'react';
+import { SubmitHandler, useForm } from 'react-hook-form';
+import { Switch } from 'src/components/Form';
+import { App, useApps } from 'src/services/apps';
 
-function classNames(...classes: any) {
-  return classes.filter(Boolean).join(' ');
-}
+export const GeneralTab = () => {
+  const { app, editApp } = useApps();
+  const { control, reset, handleSubmit } = useForm<App>({
+    defaultValues: { automaticUpdates: false },
+  });
 
-const settings = [
-  { name: 'Public access', description: 'This project would be available to anyone who has the link' },
-  { name: 'Private to Project Members', description: 'Only members of this project would be able to access' },
-  { name: 'Private to you', description: 'You are the only one able to access this project' },
-];
+  useEffect(() => {
+    if (!_.isEmpty(app)) {
+      reset(app);
+    }
 
-export const GeneralTab = () => {
-  const [selected, setSelected] = useState(settings[0]);
+    return () => {
+      reset({ automaticUpdates: false });
+    };
+  }, [app, reset]);
+
+  const onSubmit: SubmitHandler<App> = (data) => {
+    try {
+      editApp(data);
+    } catch (e: any) {
+      // continue
+    }
+  };
 
   return (
-    <div className="py-4">
-      <div>
+    <form onSubmit={handleSubmit(onSubmit)}>
+      <div className="py-4">
         <div className="md:grid md:grid-cols-3 md:gap-6">
-          <div className="md:col-span-1">
-            <h3 className="text-lg font-medium leading-6 text-gray-900">Privacy</h3>
-            <p className="mt-1 text-sm text-gray-500">Change your app privacy</p>
+          <div className="md:col-span-2">
+            <h3 className="text-lg font-medium leading-6 text-gray-900">Automatic updates</h3>
+            <p className="mt-1 text-sm text-gray-500">
+              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et nibh sit amet mauris faucibus molestie
+              gravida at orci.
+            </p>
           </div>
-
-          <div className="mt-5 md:mt-0 md:col-span-2">
-            <div className="mt-1">
-              <RadioGroup value={selected} onChange={setSelected}>
-                <RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
-                <div className="bg-white rounded-md -space-y-px">
-                  {settings.map((setting, settingIdx) => (
-                    <RadioGroup.Option
-                      key={setting.name}
-                      value={setting}
-                      className={({ checked }) =>
-                        classNames(
-                          settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
-                          settingIdx === settings.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
-                          checked ? 'bg-primary-50 border-primary-400 z-10' : 'border-gray-200',
-                          'relative border p-4 flex cursor-pointer focus:outline-none',
-                        )
-                      }
-                    >
-                      {({ active, checked }) => (
-                        <>
-                          <span
-                            className={classNames(
-                              checked ? 'bg-primary-600 border-transparent' : 'bg-white border-gray-300',
-                              active ? 'ring-2 ring-offset-2 ring-primary-100' : '',
-                              'h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center',
-                            )}
-                            aria-hidden="true"
-                          >
-                            <span className="rounded-full bg-white w-1.5 h-1.5" />
-                          </span>
-                          <div className="ml-3 flex flex-col">
-                            <RadioGroup.Label
-                              as="span"
-                              className={classNames(
-                                checked ? 'text-primary-900' : 'text-gray-900',
-                                'block text-sm font-medium',
-                              )}
-                            >
-                              {setting.name}
-                            </RadioGroup.Label>
-                            <RadioGroup.Description
-                              as="span"
-                              className={classNames(checked ? 'text-primary-900' : 'text-gray-500', 'block text-sm')}
-                            >
-                              {setting.description}
-                            </RadioGroup.Description>
-                          </div>
-                        </>
-                      )}
-                    </RadioGroup.Option>
-                  ))}
-                </div>
-              </RadioGroup>
-            </div>
+          <div className="my-auto ml-auto">
+            <Switch control={control} name="automaticUpdates" />
           </div>
         </div>
       </div>
-
-      <div className="mt-10">
-        <div className="md:grid md:grid-cols-3 md:gap-6">
-          <div className="md:col-span-1">
-            <h3 className="text-lg font-medium leading-6 text-gray-900">Notifications</h3>
-            <p className="mt-1 text-sm text-gray-500">Change you notifications settings</p>
-          </div>
-
-          <div className="mt-5 md:mt-0 md:col-span-2">
-            <fieldset className="space-y-5 -mt-4">
-              <legend className="sr-only">Notifications</legend>
-              <div className="relative flex items-start">
-                <div className="flex items-center h-5">
-                  <input
-                    id="comments"
-                    aria-describedby="comments-description"
-                    name="comments"
-                    type="checkbox"
-                    className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
-                  />
-                </div>
-                <div className="ml-3 text-sm">
-                  <label htmlFor="comments" className="font-medium text-gray-700">
-                    Comments
-                  </label>
-                  <p id="comments-description" className="text-gray-500">
-                    Get notified when someones posts a comment on a posting.
-                  </p>
-                </div>
-              </div>
-              <div>
-                <div className="relative flex items-start">
-                  <div className="flex items-center h-5">
-                    <input
-                      id="candidates"
-                      aria-describedby="candidates-description"
-                      name="candidates"
-                      type="checkbox"
-                      className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
-                    />
-                  </div>
-                  <div className="ml-3 text-sm">
-                    <label htmlFor="candidates" className="font-medium text-gray-700">
-                      Candidates
-                    </label>
-                    <p id="candidates-description" className="text-gray-500">
-                      Get notified when a candidate applies for a job.
-                    </p>
-                  </div>
-                </div>
-              </div>
-              <div>
-                <div className="relative flex items-start">
-                  <div className="flex items-center h-5">
-                    <input
-                      id="offers"
-                      aria-describedby="offers-description"
-                      name="offers"
-                      type="checkbox"
-                      className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
-                    />
-                  </div>
-                  <div className="ml-3 text-sm">
-                    <label htmlFor="offers" className="font-medium text-gray-700">
-                      Offers
-                    </label>
-                    <p id="offers-description" className="text-gray-500">
-                      Get notified when a candidate accepts or rejects an offer.
-                    </p>
-                  </div>
-                </div>
-              </div>
-            </fieldset>
-          </div>
+      <div className="py-3 sm:flex">
+        <div className="ml-auto sm:flex sm:flex-row-reverse">
+          <button
+            type="submit"
+            className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
+          >
+            Save changes
+          </button>
         </div>
       </div>
-    </div>
+    </form>
   );
 };
diff --git a/src/modules/apps/components/index.ts b/src/modules/apps/components/index.ts
index 77f373164adfe6b5d2691a1dd8b711b69fbae75b..c41720b8de1e9149394261b85b3fd65586430436 100644
--- a/src/modules/apps/components/index.ts
+++ b/src/modules/apps/components/index.ts
@@ -1,2 +1,3 @@
 export { GeneralTab } from './GeneralTab';
 export { AdvancedTab } from './AdvancedTab';
+export { AppInstallModal } from './AppInstallModal';
diff --git a/src/modules/apps/consts.tsx b/src/modules/apps/consts.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8a43f3f157945c085722bf296ca15e1f54545b00
--- /dev/null
+++ b/src/modules/apps/consts.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { CogIcon, PlusCircleIcon } from '@heroicons/react/outline';
+import _ from 'lodash';
+import { AppStatusEnum } from 'src/services/apps';
+
+export const initialEditorYaml = `luck: except
+natural: still
+near: though
+search:
+  - feature
+  - - 1980732354.689713
+    - hour
+    - butter:
+        ordinary: 995901949.8974948
+        teeth: true
+        whole:
+          - -952367353
+          - - talk: -1773961379
+              temperature: false
+              oxygen: true
+              laugh:
+                flag:
+                  in: 2144751662
+                  hospital: -1544066384.1973226
+                  law: congress
+                  great: stomach`;
+
+const tableConsts = [
+  {
+    status: AppStatusEnum.Installed,
+    colorClass: 'green-600',
+    buttonTitle: 'Configure',
+    buttonIcon: <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />,
+  },
+  {
+    status: AppStatusEnum.NotInstalled,
+    colorClass: 'gray-600',
+    buttonTitle: 'Install',
+    buttonIcon: <PlusCircleIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />,
+  },
+  {
+    status: AppStatusEnum.Installing,
+    colorClass: 'primary-600',
+    buttonTitle: null,
+    buttonIcon: null,
+  },
+];
+
+export const getConstForStatus = (appStatus: AppStatusEnum, paramName: string) => {
+  const tableConst = _.find(tableConsts, { status: appStatus });
+  return _.get(tableConst, paramName);
+};
diff --git a/src/modules/dashboard/Dashboard.tsx b/src/modules/dashboard/Dashboard.tsx
index 959b99fd1d608e9ac1546bea7cf5c106370581f9..47968ef603a359036b92330aee6356b25ad64eb5 100644
--- a/src/modules/dashboard/Dashboard.tsx
+++ b/src/modules/dashboard/Dashboard.tsx
@@ -1,13 +1,28 @@
-import React from 'react';
-import clsx from 'clsx';
-import { DASHBOARD_APPS, DASHBOARD_QUICK_ACCESS } from './consts';
-import { DashboardCard } from './components';
+/* eslint-disable react-hooks/exhaustive-deps */
+/**
+ * Page that shows only installed applications, and links to them.
+ *
+ * "Utilities" is a special section that links to the Stackspin documentation,
+ * and that shows the "Monitoring" application if it is installed.
+ */
+import React, { useEffect } from 'react';
+import { useApps } from 'src/services/apps';
+import { AppStatusEnum } from 'src/services/apps/types';
+import { DashboardCard, DashboardUtility } from './components';
+import { DASHBOARD_QUICK_ACCESS, HIDDEN_APPS, UTILITY_APPS } from './consts';
 
 export const Dashboard: React.FC = () => {
   const host = window.location.hostname;
   const splitedDomain = host.split('.');
   splitedDomain.shift();
-  const rootDomain = splitedDomain.join('.');
+  const { apps, loadApps } = useApps();
+
+  // Tell React to load the apps
+  useEffect(() => {
+    loadApps();
+
+    return () => {};
+  }, []);
 
   return (
     <div className="relative">
@@ -19,36 +34,28 @@ export const Dashboard: React.FC = () => {
 
       <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
         <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 mb-10">
-          {DASHBOARD_APPS(rootDomain!).map((app) => (
-            <DashboardCard app={app} key={app.name} />
-          ))}
+          {apps
+            .filter((app) => HIDDEN_APPS.concat(UTILITY_APPS).indexOf(app.slug) === -1)
+            .filter((app) => app.status !== AppStatusEnum.NotInstalled)
+            .map((app) => (
+              <DashboardCard app={app} key={app.name} />
+            ))}
         </div>
-
         <div className="max-w-4xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow">
           <div className="pb-4 border-b border-gray-200 sm:flex sm:items-center">
             <h3 className="text-lg leading-6 font-medium text-gray-900">Utilities</h3>
           </div>
 
           <dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2">
-            {DASHBOARD_QUICK_ACCESS(rootDomain!).map((item) => (
-              <a
-                href={item.url}
-                key={item.name}
-                target="_blank"
-                rel="noreferrer"
-                className={clsx('bg-white rounded-lg overflow-hidden sm:p-2 flex items-center group', {
-                  'opacity-40 cursor-default': !item.active,
-                })}
-              >
-                <div className="w-16 h-16 flex items-center justify-center bg-primary-100 group-hover:bg-primary-200 transition-colors rounded-lg mr-4">
-                  <item.icon className="h-6 w-6 text-primary-900" aria-hidden="true" />
-                </div>
-                <div>
-                  <dt className="truncate text-sm leading-5 font-medium">{item.name}</dt>
-                  <dd className="mt-1 text-gray-500 text-sm leading-5 font-normal">{item.description}</dd>
-                </div>
-              </a>
+            {DASHBOARD_QUICK_ACCESS.map((item) => (
+              <DashboardUtility item={item} key={item.name} />
             ))}
+            {apps
+              .filter((app) => UTILITY_APPS.indexOf(app.slug) !== -1 && app.url !== null)
+              .filter((app) => app.status !== AppStatusEnum.NotInstalled)
+              .map((app) => (
+                <DashboardUtility item={app} key={app.name} />
+              ))}
           </dl>
         </div>
       </div>
diff --git a/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx b/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx
index d8142e06151e6da786a18dbd77ab253d251dc089..1b9b820090d41765ab6ba4bb21181eb0776aee6c 100644
--- a/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx
+++ b/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx
@@ -25,7 +25,7 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
             <img
               className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0"
               src={app.assetSrc}
-              alt="Nextcloud"
+              alt={app.name}
             />
 
             <div>
diff --git a/src/modules/dashboard/components/DashboardUtility/DashboardUtility.tsx b/src/modules/dashboard/components/DashboardUtility/DashboardUtility.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8b1c2b17119a2d7d1f864331dff662769832c49f
--- /dev/null
+++ b/src/modules/dashboard/components/DashboardUtility/DashboardUtility.tsx
@@ -0,0 +1,36 @@
+import React, { useState, useEffect } from 'react';
+import ReactMarkdown from 'react-markdown';
+
+export const DashboardUtility: React.FC<any> = ({ item }: { item: any }) => {
+  const [content, setContent] = useState('');
+
+  useEffect(() => {
+    fetch(item.markdownSrc)
+      .then((res) => res.text())
+      .then((md) => {
+        return setContent(md);
+      })
+      .catch(() => {});
+  }, [item.markdownSrc]);
+
+  return (
+    <a
+      href={item.url}
+      key={item.name}
+      target="_blank"
+      rel="noreferrer"
+      className="bg-white rounded-lg overflow-hidden sm:p-2 flex items-center group"
+    >
+      <div className="w-16 h-16 flex items-center justify-center bg-primary-100 group-hover:bg-primary-200 transition-colors rounded-lg mr-4">
+        {item.icon && <item.icon className="h-6 w-6 text-primary-900" aria-hidden="true" />}
+        {item.assetSrc && <img className="h-6 w-6" src={item.assetSrc} alt={item.name} />}
+      </div>
+      <div>
+        <dt className="truncate text-sm leading-5 font-medium">{item.name}</dt>
+        <dd className="mt-1 text-gray-500 text-sm leading-5 font-normal">
+          <ReactMarkdown>{content}</ReactMarkdown>
+        </dd>
+      </div>
+    </a>
+  );
+};
diff --git a/src/modules/dashboard/components/DashboardUtility/index.ts b/src/modules/dashboard/components/DashboardUtility/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..24e4b682561aff959c8a6112f702067d47c375f7
--- /dev/null
+++ b/src/modules/dashboard/components/DashboardUtility/index.ts
@@ -0,0 +1 @@
+export { DashboardUtility } from './DashboardUtility';
diff --git a/src/modules/dashboard/components/index.ts b/src/modules/dashboard/components/index.ts
index 479e087a9b0ae50e1771641c302de4f9636a9bf9..2a1468292e7106bd4f50c42b37e8f42a27ed45d4 100644
--- a/src/modules/dashboard/components/index.ts
+++ b/src/modules/dashboard/components/index.ts
@@ -1 +1,2 @@
 export { DashboardCard } from './DashboardCard';
+export { DashboardUtility } from './DashboardUtility';
diff --git a/src/modules/dashboard/consts.ts b/src/modules/dashboard/consts.ts
index c33481a614fd511ace53e8d99c33f3ee5729972d..55984982bdcb8d6e287b32e129dbd1c1cd0ddfe2 100644
--- a/src/modules/dashboard/consts.ts
+++ b/src/modules/dashboard/consts.ts
@@ -1,51 +1,16 @@
-import { ChartBarIcon, InformationCircleIcon } from '@heroicons/react/outline';
+import { InformationCircleIcon } from '@heroicons/react/outline';
 
-export const DASHBOARD_APPS = (rootDomain: string) => [
+export const DASHBOARD_QUICK_ACCESS = [
   {
-    id: 1,
-    name: 'Nextcloud',
-    assetSrc: '/assets/nextcloud.svg',
-    markdownSrc: '/markdown/nextcloud.md',
-    url: `https://files.${rootDomain}`,
-  },
-  {
-    id: 2,
-    name: 'Wekan',
-    assetSrc: '/assets/wekan.svg',
-    markdownSrc: '/markdown/wekan.md',
-    url: `https://wekan.${rootDomain}`,
-  },
-  {
-    id: 3,
-    name: 'Zulip',
-    assetSrc: '/assets/zulip.svg',
-    markdownSrc: '/markdown/zulip.md',
-    url: `https://zulip.${rootDomain}`,
-  },
-  {
-    id: 4,
-    name: 'Wordpress',
-    assetSrc: '/assets/wordpress.svg',
-    markdownSrc: '/markdown/wordpress.md',
-    url: `https://www.${rootDomain}`,
-  },
-];
-
-export const DASHBOARD_QUICK_ACCESS = (rootDomain: string) => [
-  {
-    id: 1,
-    name: 'Monitoring →',
-    url: `https://grafana.${rootDomain}`,
-    description: 'Monitor your system with Grafana',
-    icon: ChartBarIcon,
-    active: true,
-  },
-  {
-    id: 2,
-    name: 'Support →',
+    name: 'Support',
     url: 'https://docs.stackspin.net',
-    description: 'Access documentation and forum',
+    markdownSrc: '/markdown/support.md',
     icon: InformationCircleIcon,
-    active: true,
   },
 ];
+
+/** Apps that should not be shown on the dashboard */
+export const HIDDEN_APPS = ['dashboard'];
+
+/** Apps that should be shown under "Utilities" */
+export const UTILITY_APPS = ['monitoring'];
diff --git a/src/modules/login/Login.tsx b/src/modules/login/Login.tsx
index 22f2a043686428ee9025c97fc2e80f81950b84ee..87be39e1acb6307d70ead6e9432a07b50c48d5d6 100644
--- a/src/modules/login/Login.tsx
+++ b/src/modules/login/Login.tsx
@@ -1,3 +1,6 @@
+/**
+ * Login page that starts the OAuth2 authentication flow.
+ */
 import React from 'react';
 import clsx from 'clsx';
 import { LockClosedIcon } from '@heroicons/react/solid';
diff --git a/src/modules/users/Users.tsx b/src/modules/users/Users.tsx
index 41cfafbf3444a685267465d88df1dd9938bee7dc..5d45dd526be0e7668e3d241c1bafb72e3b7108be 100644
--- a/src/modules/users/Users.tsx
+++ b/src/modules/users/Users.tsx
@@ -1,4 +1,9 @@
 /* eslint-disable react-hooks/exhaustive-deps */
+/**
+ * This page shows a table of all users. It is only available for Admin users.
+ *
+ * Admin users can add one or more users, or edit a user.
+ */
 import React, { useState, useCallback, useEffect, useMemo } from 'react';
 import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
 import { CogIcon, TrashIcon } from '@heroicons/react/outline';
diff --git a/src/redux/store.ts b/src/redux/store.ts
index ff46366bb70a13369bbedc183e2bcdfdff97dbcb..8e1c58d7e4fe763ff66996181348cd2c0c8db0e4 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -6,6 +6,7 @@ import storage from 'redux-persist/lib/storage';
 import { reducer as authReducer } from 'src/services/auth';
 
 import usersReducer from 'src/services/users/redux/reducers';
+import appsReducer from 'src/services/apps/redux/reducers';
 import { State } from './types';
 
 const persistConfig = {
@@ -17,6 +18,7 @@ const persistConfig = {
 const appReducer = combineReducers<State>({
   auth: authReducer,
   users: usersReducer,
+  apps: appsReducer,
 });
 
 const persistedReducer = persistReducer(persistConfig, appReducer);
diff --git a/src/redux/types.ts b/src/redux/types.ts
index 4d42236f498e361442d09523ab0959f90996e7e6..85057964407d1039c485391f99b46fd23b9ca76f 100644
--- a/src/redux/types.ts
+++ b/src/redux/types.ts
@@ -2,10 +2,12 @@ import { Store } from 'redux';
 
 import { AuthState } from 'src/services/auth/redux';
 import { UsersState } from 'src/services/users/redux';
+import { AppsState } from 'src/services/apps/redux';
 
 export interface AppStore extends Store, State {}
 
 export interface State {
   auth: AuthState;
   users: UsersState;
+  apps: AppsState;
 }
diff --git a/src/services/apps/hooks/index.ts b/src/services/apps/hooks/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f3648cecfa8b1570cf48d1ded3364f3a696464ce
--- /dev/null
+++ b/src/services/apps/hooks/index.ts
@@ -0,0 +1 @@
+export { useApps } from './use-apps';
diff --git a/src/services/apps/hooks/use-apps.ts b/src/services/apps/hooks/use-apps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..521df878a8ee74d1a3f4ccfecd0ed514f909399a
--- /dev/null
+++ b/src/services/apps/hooks/use-apps.ts
@@ -0,0 +1,48 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchApps, fetchAppBySlug, updateApp, installAppBySlug, clearCurrentApp, deleteApp } from '../redux';
+import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors';
+
+export function useApps() {
+  const dispatch = useDispatch();
+  const apps = useSelector(getApps);
+  const app = useSelector(getCurrentApp);
+  const appLoading = useSelector(getAppLoading);
+  const appTableLoading = useSelector(getAppsLoading);
+
+  function loadApps() {
+    return dispatch(fetchApps());
+  }
+
+  function loadApp(slug: string) {
+    return dispatch(fetchAppBySlug(slug));
+  }
+
+  function editApp(data: any) {
+    return dispatch(updateApp(data));
+  }
+
+  function installApp(data: any) {
+    return dispatch(installAppBySlug(data));
+  }
+
+  function disableApp(data: any) {
+    return dispatch(deleteApp(data));
+  }
+
+  function clearSelectedApp() {
+    return dispatch(clearCurrentApp());
+  }
+
+  return {
+    apps,
+    app,
+    loadApp,
+    loadApps,
+    editApp,
+    appLoading,
+    appTableLoading,
+    installApp,
+    disableApp,
+    clearSelectedApp,
+  };
+}
diff --git a/src/services/apps/index.ts b/src/services/apps/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..84e225b7bed326662bdd4c839b5912f60a6eb65c
--- /dev/null
+++ b/src/services/apps/index.ts
@@ -0,0 +1,3 @@
+export * from './types';
+export { reducer } from './redux';
+export { useApps } from './hooks';
diff --git a/src/services/apps/redux/actions.ts b/src/services/apps/redux/actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..09ebe36970760bdd9ddd6e495b21b57e3cf4d6ce
--- /dev/null
+++ b/src/services/apps/redux/actions.ts
@@ -0,0 +1,155 @@
+import { Dispatch } from 'redux';
+import { showToast, ToastType } from 'src/common/util/show-toast';
+import { performApiCall } from 'src/services/api';
+import { transformAppRequest, transformApp, transformInstallAppRequest } from '../transformations';
+
+export enum AppActionTypes {
+  FETCH_APPS = 'apps/fetch_apps',
+  FETCH_APP = 'apps/fetch_app',
+  UPDATE_APP = 'apps/update_app',
+  INSTALL_APP = 'apps/install_app',
+  DELETE_APP = 'apps/delete_app',
+  CLEAR_APP = 'apps/clear_app',
+  SET_APP_LOADING = 'apps/app_loading',
+  SET_APPS_LOADING = 'apps/apps_loading',
+}
+
+export const setAppsLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
+  dispatch({
+    type: AppActionTypes.SET_APPS_LOADING,
+    payload: isLoading,
+  });
+};
+
+export const setAppLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
+  dispatch({
+    type: AppActionTypes.SET_APP_LOADING,
+    payload: isLoading,
+  });
+};
+
+export const fetchApps = () => async (dispatch: Dispatch<any>) => {
+  dispatch(setAppsLoading(true));
+
+  try {
+    const { data } = await performApiCall({
+      path: '/apps',
+      method: 'GET',
+    });
+
+    dispatch({
+      type: AppActionTypes.FETCH_APPS,
+      payload: data.map(transformApp),
+    });
+  } catch (err) {
+    console.error(err);
+  }
+
+  dispatch(setAppsLoading(false));
+};
+
+export const fetchAppBySlug = (slug: string) => async (dispatch: Dispatch<any>) => {
+  dispatch(setAppLoading(true));
+
+  try {
+    const { data } = await performApiCall({
+      path: `/apps/${slug}`,
+      method: 'GET',
+    });
+
+    dispatch({
+      type: AppActionTypes.FETCH_APP,
+      payload: transformApp(data),
+    });
+  } catch (err) {
+    console.error(err);
+  }
+
+  dispatch(setAppLoading(false));
+};
+
+export const updateApp = (app: any) => async (dispatch: Dispatch<any>) => {
+  dispatch(setAppLoading(true));
+
+  try {
+    const { data } = await performApiCall({
+      path: `/apps/${app.slug}`,
+      method: 'PUT',
+      body: transformAppRequest(app),
+    });
+
+    dispatch({
+      type: AppActionTypes.UPDATE_APP,
+      payload: transformApp(data),
+    });
+
+    showToast('App is updated!', ToastType.Success);
+
+    dispatch(fetchApps());
+  } catch (err) {
+    console.error(err);
+  }
+
+  dispatch(setAppLoading(false));
+};
+
+export const installAppBySlug = (app: any) => async (dispatch: Dispatch<any>) => {
+  dispatch(setAppLoading(true));
+
+  try {
+    const { data } = await performApiCall({
+      path: `/apps/${app.slug}/install`,
+      method: 'PATCH',
+      body: transformInstallAppRequest(app),
+    });
+
+    dispatch({
+      type: AppActionTypes.INSTALL_APP,
+      payload: transformApp(data),
+    });
+
+    showToast('App installing...', ToastType.Success);
+
+    dispatch(fetchApps());
+  } catch (err: any) {
+    dispatch(setAppLoading(false));
+    showToast(`${err}`, ToastType.Error);
+    throw err;
+  }
+
+  dispatch(setAppLoading(false));
+};
+
+export const deleteApp = (appData: any) => async (dispatch: Dispatch<any>) => {
+  dispatch(setAppLoading(true));
+
+  try {
+    await performApiCall({
+      path: `/apps/${appData.slug}`,
+      method: 'DELETE',
+      body: { remove_app_data: appData.removeAppData },
+    });
+
+    dispatch({
+      type: AppActionTypes.DELETE_APP,
+      payload: {},
+    });
+
+    showToast('App disabled', ToastType.Success);
+
+    dispatch(fetchApps());
+  } catch (err: any) {
+    dispatch(setAppLoading(false));
+    showToast(`${err}`, ToastType.Error);
+    throw err;
+  }
+
+  dispatch(setAppLoading(false));
+};
+
+export const clearCurrentApp = () => (dispatch: Dispatch<any>) => {
+  dispatch({
+    type: AppActionTypes.CLEAR_APP,
+    payload: {},
+  });
+};
diff --git a/src/services/apps/redux/index.ts b/src/services/apps/redux/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..88a7a71c10383e2778b42f7f81308310c63639e1
--- /dev/null
+++ b/src/services/apps/redux/index.ts
@@ -0,0 +1,4 @@
+export * from './actions';
+export { default as reducer } from './reducers';
+export { getApps } from './selectors';
+export * from './types';
diff --git a/src/services/apps/redux/reducers.ts b/src/services/apps/redux/reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..26736ae5e20562473e84116c2d59737d50238756
--- /dev/null
+++ b/src/services/apps/redux/reducers.ts
@@ -0,0 +1,45 @@
+import { AppActionTypes } from './actions';
+
+const initialAppsState: any = {
+  apps: [],
+  app: {},
+  appModalLoading: false,
+  appsLoading: false,
+};
+
+const appsReducer = (state: any = initialAppsState, action: any) => {
+  switch (action.type) {
+    case AppActionTypes.FETCH_APPS:
+      return {
+        ...state,
+        apps: action.payload,
+      };
+    case AppActionTypes.SET_APP_LOADING:
+      return {
+        ...state,
+        appLoading: action.payload,
+      };
+    case AppActionTypes.SET_APPS_LOADING:
+      return {
+        ...state,
+        appsLoading: action.payload,
+      };
+    case AppActionTypes.FETCH_APP:
+    case AppActionTypes.UPDATE_APP:
+    case AppActionTypes.INSTALL_APP:
+      return {
+        ...state,
+        currentApp: action.payload,
+      };
+    case AppActionTypes.CLEAR_APP:
+    case AppActionTypes.DELETE_APP:
+      return {
+        ...state,
+        currentApp: {},
+      };
+    default:
+      return state;
+  }
+};
+
+export default appsReducer;
diff --git a/src/services/apps/redux/selectors.ts b/src/services/apps/redux/selectors.ts
new file mode 100644
index 0000000000000000000000000000000000000000..751f24c897eef496ee8d9ebeb665fc9c15f16ee7
--- /dev/null
+++ b/src/services/apps/redux/selectors.ts
@@ -0,0 +1,6 @@
+import { State } from 'src/redux';
+
+export const getApps = (state: State) => state.apps.apps;
+export const getCurrentApp = (state: State) => state.apps.currentApp;
+export const getAppLoading = (state: State) => state.apps.appLoading;
+export const getAppsLoading = (state: State) => state.apps.appsLoading;
diff --git a/src/services/apps/redux/types.ts b/src/services/apps/redux/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..82a6c262f2cf5ef594c9626f789b6d2fffb57c31
--- /dev/null
+++ b/src/services/apps/redux/types.ts
@@ -0,0 +1,14 @@
+import { ApiStatus } from 'src/services/api/redux';
+
+import { App } from '../types';
+
+export interface CurrentUserState extends App {
+  _status: ApiStatus;
+}
+
+export interface AppsState {
+  currentApp: CurrentUserState;
+  apps: App[];
+  appLoading: boolean;
+  appsLoading: boolean;
+}
diff --git a/src/services/apps/transformations.ts b/src/services/apps/transformations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6b98a878cd2190ff4021a2aa498c6070042c6be6
--- /dev/null
+++ b/src/services/apps/transformations.ts
@@ -0,0 +1,34 @@
+import { App, AppStatus, AppStatusEnum } from './types';
+
+const transformAppStatus = (status: AppStatus) => {
+  if (status.installed && status.ready) return AppStatusEnum.Installed;
+  if (status.installed && !status.ready) return AppStatusEnum.Installing;
+  return AppStatusEnum.NotInstalled;
+};
+
+export const transformApp = (response: any): App => {
+  return {
+    id: response.id ?? '',
+    name: response.name ?? '',
+    slug: response.slug ?? '',
+    status: transformAppStatus(response.status),
+    url: response.url,
+    automaticUpdates: response.automatic_updates,
+    assetSrc: `/assets/${response.slug}.svg`,
+    markdownSrc: `/markdown/${response.slug}.md`,
+  };
+};
+
+export const transformAppRequest = (data: App) => {
+  return {
+    automatic_updates: data.automaticUpdates,
+    configuration: data.configuration,
+  };
+};
+
+export const transformInstallAppRequest = (data: App) => {
+  return {
+    url: data.url,
+    configuration: data.configuration,
+  };
+};
diff --git a/src/services/apps/types.ts b/src/services/apps/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9c5e0e22ca1ecaca969c6e111d4d281a20678459
--- /dev/null
+++ b/src/services/apps/types.ts
@@ -0,0 +1,29 @@
+export interface App {
+  id: number;
+  name: string;
+  slug: string;
+  status?: AppStatusEnum;
+  url: string;
+  automaticUpdates: boolean;
+  configuration?: string;
+  assetSrc?: string;
+  markdownSrc?: string;
+}
+
+export interface DisableAppForm {
+  slug: string;
+  removeAppData: boolean;
+}
+
+export interface AppStatus {
+  installed: boolean;
+  ready: boolean;
+  message: string;
+}
+
+export enum AppStatusEnum {
+  NotInstalled = 'Not installed',
+  Installed = 'Installed',
+  Installing = 'Installing',
+  External = 'External',
+}
diff --git a/yarn.lock b/yarn.lock
index 7eefd82bcb4976b58fbe32bba31690447d13c118..b8d82ca5206b4926492578ea8023735d190bd99c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1862,6 +1862,11 @@
     jest-diff "^26.0.0"
     pretty-format "^26.0.0"
 
+"@types/js-yaml@^4.0.5":
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
+  integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
+
 "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8":
   version "7.0.9"
   resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz"