diff --git a/app/controllers/api/v1/accounts/dashboard_apps_controller.rb b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
new file mode 100644
index 000000000..a8d7ebcb9
--- /dev/null
+++ b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb
@@ -0,0 +1,44 @@
+class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseController
+  before_action :fetch_dashboard_apps, except: [:create]
+  before_action :fetch_dashboard_app, only: [:show, :update, :destroy]
+
+  def index; end
+
+  def show; end
+
+  def create
+    @dashboard_app = Current.account.dashboard_apps.create!(
+      permitted_payload.merge(user_id: Current.user.id)
+    )
+  end
+
+  def update
+    @dashboard_app.update!(permitted_payload)
+  end
+
+  def destroy
+    @dashboard_app.destroy!
+    head :no_content
+  end
+
+  private
+
+  def fetch_dashboard_apps
+    @dashboard_apps = Current.account.dashboard_apps
+  end
+
+  def fetch_dashboard_app
+    @dashboard_app = @dashboard_apps.find(permitted_params[:id])
+  end
+
+  def permitted_payload
+    params.require(:dashboard_app).permit(
+      :title,
+      content: [:url, :type]
+    )
+  end
+
+  def permitted_params
+    params.permit(:id)
+  end
+end
diff --git a/app/javascript/dashboard/api/dashboardApps.js b/app/javascript/dashboard/api/dashboardApps.js
new file mode 100644
index 000000000..b22ab698e
--- /dev/null
+++ b/app/javascript/dashboard/api/dashboardApps.js
@@ -0,0 +1,9 @@
+import ApiClient from './ApiClient';
+
+class DashboardAppsAPI extends ApiClient {
+  constructor() {
+    super('dashboard_apps', { accountScoped: true });
+  }
+}
+
+export default new DashboardAppsAPI();
diff --git a/app/javascript/dashboard/api/specs/dashboardApps.spec.js b/app/javascript/dashboard/api/specs/dashboardApps.spec.js
new file mode 100644
index 000000000..f3196ae2a
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/dashboardApps.spec.js
@@ -0,0 +1,13 @@
+import dashboardAppsAPI from '../dashboardApps';
+import ApiClient from '../ApiClient';
+
+describe('#dashboardAppsAPI', () => {
+  it('creates correct instance', () => {
+    expect(dashboardAppsAPI).toBeInstanceOf(ApiClient);
+    expect(dashboardAppsAPI).toHaveProperty('get');
+    expect(dashboardAppsAPI).toHaveProperty('show');
+    expect(dashboardAppsAPI).toHaveProperty('create');
+    expect(dashboardAppsAPI).toHaveProperty('update');
+    expect(dashboardAppsAPI).toHaveProperty('delete');
+  });
+});
diff --git a/app/javascript/dashboard/components/widgets/DashboardApp/Frame.vue b/app/javascript/dashboard/components/widgets/DashboardApp/Frame.vue
new file mode 100644
index 000000000..0c2c0e8ed
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/DashboardApp/Frame.vue
@@ -0,0 +1,64 @@
+
+  
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
index 4849c2e97..494bd5c82 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
@@ -6,7 +6,20 @@
       :is-contact-panel-open="isContactPanelOpen"
       @contact-panel-toggle="onToggleContactPanel"
     />
-    
 
 
@@ -96,6 +138,11 @@ export default {
   background: var(--color-background-light);
 }
 
+.dashboard-app--tabs {
+  background: var(--white);
+  margin-top: -1px;
+}
+
 .messages-and-sidebar {
   display: flex;
   background: var(--color-background-light);
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index e545ce0d2..c38fb16a4 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -1,6 +1,7 @@
 {
   "CONVERSATION": {
     "404": "Please select a conversation from left pane",
+    "DASHBOARD_APP_TAB_MESSAGES": "Messages",
     "UNVERIFIED_SESSION": "The identity of this user is not verified",
     "NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
     "NO_MESSAGE_2": " to send a message to your page!",
diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js
index 88c36cdf8..5cd1a0112 100755
--- a/app/javascript/dashboard/store/index.js
+++ b/app/javascript/dashboard/store/index.js
@@ -3,7 +3,9 @@ import Vuex from 'vuex';
 
 import accounts from './modules/accounts';
 import agents from './modules/agents';
+import attributes from './modules/attributes';
 import auth from './modules/auth';
+import automations from './modules/automations';
 import campaigns from './modules/campaigns';
 import cannedResponse from './modules/cannedResponse';
 import contactConversations from './modules/contactConversations';
@@ -18,6 +20,8 @@ import conversationSearch from './modules/conversationSearch';
 import conversationStats from './modules/conversationStats';
 import conversationTypingStatus from './modules/conversationTypingStatus';
 import csat from './modules/csat';
+import customViews from './modules/customViews';
+import dashboardApps from './modules/dashboardApps';
 import globalConfig from 'shared/store/globalConfig';
 import inboxAssignableAgents from './modules/inboxAssignableAgents';
 import inboxes from './modules/inboxes';
@@ -30,16 +34,15 @@ import teamMembers from './modules/teamMembers';
 import teams from './modules/teams';
 import userNotificationSettings from './modules/userNotificationSettings';
 import webhooks from './modules/webhooks';
-import attributes from './modules/attributes';
-import automations from './modules/automations';
-import customViews from './modules/customViews';
 
 Vue.use(Vuex);
 export default new Vuex.Store({
   modules: {
     accounts,
     agents,
+    attributes,
     auth,
+    automations,
     campaigns,
     cannedResponse,
     contactConversations,
@@ -54,6 +57,8 @@ export default new Vuex.Store({
     conversationStats,
     conversationTypingStatus,
     csat,
+    customViews,
+    dashboardApps,
     globalConfig,
     inboxAssignableAgents,
     inboxes,
@@ -66,8 +71,5 @@ export default new Vuex.Store({
     teams,
     userNotificationSettings,
     webhooks,
-    attributes,
-    automations,
-    customViews,
   },
 });
diff --git a/app/javascript/dashboard/store/modules/dashboardApps.js b/app/javascript/dashboard/store/modules/dashboardApps.js
new file mode 100644
index 000000000..fda1f4de4
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/dashboardApps.js
@@ -0,0 +1,54 @@
+import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
+import types from '../mutation-types';
+import DashboardAppsAPI from '../../api/dashboardApps';
+
+export const state = {
+  records: [],
+  uiFlags: {
+    isFetching: false,
+    isCreating: false,
+    isDeleting: false,
+  },
+};
+
+export const getters = {
+  getUIFlags(_state) {
+    return _state.uiFlags;
+  },
+  getRecords(_state) {
+    return _state.records;
+  },
+};
+
+export const actions = {
+  get: async function getDashboardApps({ commit }) {
+    commit(types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: true });
+    try {
+      const response = await DashboardAppsAPI.get();
+      commit(types.SET_DASHBOARD_APPS, response.data);
+    } catch (error) {
+      // Ignore error
+    } finally {
+      commit(types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: false });
+    }
+  },
+};
+
+export const mutations = {
+  [types.SET_DASHBOARD_APPS_UI_FLAG](_state, data) {
+    _state.uiFlags = {
+      ..._state.uiFlags,
+      ...data,
+    };
+  },
+
+  [types.SET_DASHBOARD_APPS]: MutationHelpers.set,
+};
+
+export default {
+  namespaced: true,
+  actions,
+  state,
+  getters,
+  mutations,
+};
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/actions.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/actions.spec.js
new file mode 100644
index 000000000..d24d879bf
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/actions.spec.js
@@ -0,0 +1,21 @@
+import axios from 'axios';
+import { actions } from '../../dashboardApps';
+import types from '../../../mutation-types';
+
+const commit = jest.fn();
+global.axios = axios;
+jest.mock('axios');
+
+describe('#actions', () => {
+  describe('#get', () => {
+    it('sends correct actions if API is success', async () => {
+      axios.get.mockResolvedValue({ data: [{ title: 'Title 1' }] });
+      await actions.get({ commit });
+      expect(commit.mock.calls).toEqual([
+        [types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: true }],
+        [types.SET_DASHBOARD_APPS, [{ title: 'Title 1' }]],
+        [types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: false }],
+      ]);
+    });
+  });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
new file mode 100644
index 000000000..c27788581
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/getters.spec.js
@@ -0,0 +1,29 @@
+import { getters } from '../../dashboardApps';
+
+describe('#getters', () => {
+  it('getRecords', () => {
+    const state = {
+      records: [
+        {
+          title: '1',
+          content: [{ link: 'https://google.com', type: 'frame' }],
+        },
+      ],
+    };
+    expect(getters.getRecords(state)).toEqual(state.records);
+  });
+  it('getUIFlags', () => {
+    const state = {
+      uiFlags: {
+        isFetching: true,
+        isCreating: false,
+        isDeleting: false,
+      },
+    };
+    expect(getters.getUIFlags(state)).toEqual({
+      isFetching: true,
+      isCreating: false,
+      isDeleting: false,
+    });
+  });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/dashboardApps/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/dashboardApps/mutations.spec.js
new file mode 100644
index 000000000..aaf4da33e
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/dashboardApps/mutations.spec.js
@@ -0,0 +1,20 @@
+import types from '../../../mutation-types';
+import { mutations } from '../../dashboardApps';
+
+describe('#mutations', () => {
+  describe('#SET_DASHBOARD_APPS_UI_FLAG', () => {
+    it('set dashboard app ui flags', () => {
+      const state = { uiFlags: { isCreating: false, isUpdating: false } };
+      mutations[types.SET_DASHBOARD_APPS_UI_FLAG](state, { isUpdating: true });
+      expect(state.uiFlags).toEqual({ isCreating: false, isUpdating: true });
+    });
+  });
+
+  describe('#SET_DASHBOARD_APPS', () => {
+    it('set dashboard records', () => {
+      const state = { records: [{ title: 'Title 0' }] };
+      mutations[types.SET_DASHBOARD_APPS](state, [{ title: 'Title 1' }]);
+      expect(state.records).toEqual([{ title: 'Title 1' }]);
+    });
+  });
+});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index f89a5205d..b78e468a1 100755
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -210,4 +210,8 @@ export default {
   SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW',
   ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
   DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
+
+  // Dashboard Apps
+  SET_DASHBOARD_APPS_UI_FLAG: 'SET_DASHBOARD_APPS_UI_FLAG',
+  SET_DASHBOARD_APPS: 'SET_DASHBOARD_APPS',
 };
diff --git a/app/javascript/widget/store/modules/contacts.js b/app/javascript/widget/store/modules/contacts.js
index 587b84ac6..b82cda8ad 100644
--- a/app/javascript/widget/store/modules/contacts.js
+++ b/app/javascript/widget/store/modules/contacts.js
@@ -61,7 +61,10 @@ export const actions = {
         dispatch('conversationAttributes/getAttributes', {}, { root: true });
       }
     } catch (error) {
-      const data = error && error.response && error.response.data ? error.response.data : error
+      const data =
+        error && error.response && error.response.data
+          ? error.response.data
+          : error;
       IFrameHelper.sendMessage({
         event: 'error',
         errorType: SET_USER_ERROR,
diff --git a/app/models/account.rb b/app/models/account.rb
index b14887957..7dd1cc229 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -39,27 +39,31 @@ class Account < ApplicationRecord
   has_many :agent_bot_inboxes, dependent: :destroy_async
   has_many :agent_bots, dependent: :destroy_async
   has_many :api_channels, dependent: :destroy_async, class_name: '::Channel::Api'
+  has_many :articles, dependent: :destroy_async, class_name: '::Article'
+  has_many :automation_rules, dependent: :destroy
   has_many :campaigns, dependent: :destroy_async
   has_many :canned_responses, dependent: :destroy_async
+  has_many :categories, dependent: :destroy_async, class_name: '::Category'
   has_many :contacts, dependent: :destroy_async
   has_many :conversations, dependent: :destroy_async
   has_many :csat_survey_responses, dependent: :destroy_async
   has_many :custom_attribute_definitions, dependent: :destroy_async
   has_many :custom_filters, dependent: :destroy_async
+  has_many :dashboard_apps, dependent: :destroy
   has_many :data_imports, dependent: :destroy_async
   has_many :email_channels, dependent: :destroy_async, class_name: '::Channel::Email'
   has_many :facebook_pages, dependent: :destroy_async, class_name: '::Channel::FacebookPage'
   has_many :hooks, dependent: :destroy_async, class_name: 'Integrations::Hook'
   has_many :inboxes, dependent: :destroy_async
-  has_many :articles, dependent: :destroy_async, class_name: '::Article'
-  has_many :categories, dependent: :destroy_async, class_name: '::Category'
-  has_many :portals, dependent: :destroy_async, class_name: '::Portal'
   has_many :labels, dependent: :destroy_async
   has_many :line_channels, dependent: :destroy_async, class_name: '::Channel::Line'
   has_many :mentions, dependent: :destroy_async
   has_many :messages, dependent: :destroy_async
   has_many :notes, dependent: :destroy_async
   has_many :notification_settings, dependent: :destroy_async
+  has_many :notifications, dependent: :destroy
+  has_many :portals, dependent: :destroy_async, class_name: '::Portal'
+  has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
   has_many :teams, dependent: :destroy_async
   has_many :telegram_bots, dependent: :destroy_async
   has_many :telegram_channels, dependent: :destroy_async, class_name: '::Channel::Telegram'
@@ -69,10 +73,7 @@ class Account < ApplicationRecord
   has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
   has_many :webhooks, dependent: :destroy_async
   has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
-  has_many :sms_channels, dependent: :destroy_async, class_name: '::Channel::Sms'
   has_many :working_hours, dependent: :destroy_async
-  has_many :automation_rules, dependent: :destroy
-  has_many :notifications, dependent: :destroy
 
   has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
 
diff --git a/app/models/dashboard_app.rb b/app/models/dashboard_app.rb
new file mode 100644
index 000000000..1f1b73d5c
--- /dev/null
+++ b/app/models/dashboard_app.rb
@@ -0,0 +1,49 @@
+# == Schema Information
+#
+# Table name: dashboard_apps
+#
+#  id         :bigint           not null, primary key
+#  content    :jsonb
+#  title      :string           not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#  account_id :bigint           not null
+#  user_id    :bigint
+#
+# Indexes
+#
+#  index_dashboard_apps_on_account_id  (account_id)
+#  index_dashboard_apps_on_user_id     (user_id)
+#
+# Foreign Keys
+#
+#  fk_rails_...  (account_id => accounts.id)
+#  fk_rails_...  (user_id => users.id)
+#
+class DashboardApp < ApplicationRecord
+  belongs_to :user
+  belongs_to :account
+  validate :validate_content
+
+  private
+
+  def validate_content
+    has_invalid_data = self[:content].blank? || !self[:content].is_a?(Array)
+    self[:content] = [] if has_invalid_data
+
+    content_schema = {
+      'type' => 'array',
+      'items' => {
+        'type' => 'object',
+        'required' => %w[url type],
+        'properties' => {
+          'type' => { 'enum': ['frame'] },
+          'url' => { 'type': 'string', 'format' => 'uri' }
+        }
+      },
+      'additionalProperties' => false,
+      'minItems' => 1
+    }
+    errors.add(:content, ': Invalid data') unless JSONSchemer.schema(content_schema.to_json).valid?(self[:content])
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6c3349473..534ebea21 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -83,14 +83,15 @@ class User < ApplicationRecord
   has_many :invitees, through: :account_users, class_name: 'User', foreign_key: 'inviter_id', source: :inviter, dependent: :nullify
 
   has_many :custom_filters, dependent: :destroy_async
+  has_many :dashboard_apps, dependent: :nullify
   has_many :mentions, dependent: :destroy_async
   has_many :notes, dependent: :nullify
   has_many :notification_settings, dependent: :destroy_async
   has_many :notification_subscriptions, dependent: :destroy_async
   has_many :notifications, dependent: :destroy_async
+  has_many :portals, through: :portals_members
   has_many :team_members, dependent: :destroy_async
   has_many :teams, through: :team_members
-  has_many :portals, through: :portals_members
 
   before_validation :set_password_and_uid, on: :create
 
diff --git a/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder
new file mode 100644
index 000000000..fd21053d3
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/create.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app
diff --git a/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder
new file mode 100644
index 000000000..d7e1f5a06
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/index.json.jbuilder
@@ -0,0 +1,3 @@
+json.array! @dashboard_apps do |dashboard_app|
+  json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: dashboard_app
+end
diff --git a/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder
new file mode 100644
index 000000000..fd21053d3
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app
diff --git a/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder b/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder
new file mode 100644
index 000000000..fd21053d3
--- /dev/null
+++ b/app/views/api/v1/accounts/dashboard_apps/update.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/models/dashboard_app.json.jbuilder', resource: @dashboard_app
diff --git a/app/views/api/v1/models/_dashboard_app.json.jbuilder b/app/views/api/v1/models/_dashboard_app.json.jbuilder
new file mode 100644
index 000000000..f8632d28a
--- /dev/null
+++ b/app/views/api/v1/models/_dashboard_app.json.jbuilder
@@ -0,0 +1,4 @@
+json.id resource.id
+json.title resource.title
+json.content resource.content
+json.created_at resource.created_at
diff --git a/config/routes.rb b/config/routes.rb
index b2029a739..a09311b3e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,7 +58,7 @@ Rails.application.routes.draw do
             post :attach_file, on: :collection
           end
           resources :campaigns, only: [:index, :create, :show, :update, :destroy]
-
+          resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
           namespace :channels do
             resource :twilio_channel, only: [:create]
           end
diff --git a/db/migrate/20220525141844_create_dashboard_apps.rb b/db/migrate/20220525141844_create_dashboard_apps.rb
new file mode 100644
index 000000000..0e4fee9e2
--- /dev/null
+++ b/db/migrate/20220525141844_create_dashboard_apps.rb
@@ -0,0 +1,11 @@
+class CreateDashboardApps < ActiveRecord::Migration[6.1]
+  def change
+    create_table :dashboard_apps do |t|
+      t.string :title, null: false
+      t.jsonb :content, default: []
+      t.references :account, null: false, foreign_key: true
+      t.references :user, foreign_key: true
+      t.timestamps
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e7382572e..4e2ba8095 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2022_05_13_145010) do
+ActiveRecord::Schema.define(version: 2022_05_25_141844) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pg_stat_statements"
@@ -441,6 +441,17 @@ ActiveRecord::Schema.define(version: 2022_05_13_145010) do
     t.index ["user_id"], name: "index_custom_filters_on_user_id"
   end
 
+  create_table "dashboard_apps", force: :cascade do |t|
+    t.string "title", null: false
+    t.jsonb "content", default: []
+    t.bigint "account_id", null: false
+    t.bigint "user_id"
+    t.datetime "created_at", precision: 6, null: false
+    t.datetime "updated_at", precision: 6, null: false
+    t.index ["account_id"], name: "index_dashboard_apps_on_account_id"
+    t.index ["user_id"], name: "index_dashboard_apps_on_user_id"
+  end
+
   create_table "data_imports", force: :cascade do |t|
     t.bigint "account_id", null: false
     t.string "data_type", null: false
@@ -817,6 +828,8 @@ ActiveRecord::Schema.define(version: 2022_05_13_145010) do
   add_foreign_key "csat_survey_responses", "conversations", on_delete: :cascade
   add_foreign_key "csat_survey_responses", "messages", on_delete: :cascade
   add_foreign_key "csat_survey_responses", "users", column: "assigned_agent_id", on_delete: :cascade
+  add_foreign_key "dashboard_apps", "accounts"
+  add_foreign_key "dashboard_apps", "users"
   add_foreign_key "data_imports", "accounts", on_delete: :cascade
   add_foreign_key "mentions", "conversations", on_delete: :cascade
   add_foreign_key "mentions", "users", on_delete: :cascade
diff --git a/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
new file mode 100644
index 000000000..9a6b0fa67
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb
@@ -0,0 +1,158 @@
+require 'rails_helper'
+
+RSpec.describe 'DashboardAppsController', type: :request do
+  let(:account) { create(:account) }
+
+  describe 'GET /api/v1/accounts/{account.id}/dashboard_apps' do
+    context 'when it is an unauthenticated user' do
+      it 'returns unauthorized' do
+        get "/api/v1/accounts/#{account.id}/dashboard_apps"
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+
+    context 'when it is an authenticated user' do
+      let(:user) { create(:user, account: account) }
+      let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
+
+      it 'returns all dashboard_apps in the account' do
+        get "/api/v1/accounts/#{account.id}/dashboard_apps",
+            headers: user.create_new_auth_token,
+            as: :json
+
+        expect(response).to have_http_status(:success)
+        response_body = JSON.parse(response.body)
+        expect(response_body.first['title']).to eq(dashboard_app.title)
+        expect(response_body.first['content']).to eq(dashboard_app.content)
+      end
+    end
+  end
+
+  describe 'GET /api/v1/accounts/{account.id}/dashboard_apps/:id' do
+    let(:user) { create(:user, account: account) }
+    let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
+
+    context 'when it is an unauthenticated user' do
+      it 'returns unauthorized' do
+        get "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}"
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+
+    context 'when it is an authenticated user' do
+      it 'shows the dashboard app' do
+        get "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
+            headers: user.create_new_auth_token,
+            as: :json
+
+        expect(response).to have_http_status(:success)
+        expect(response.body).to include(dashboard_app.title)
+      end
+    end
+  end
+
+  describe 'POST /api/v1/accounts/{account.id}/dashboard_apps' do
+    let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } }
+    let(:invalid_type_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'dda', url: 'https://link.com' }] } } }
+    let(:invalid_url_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'com' }] } } }
+
+    context 'when it is an unauthenticated user' do
+      it 'returns unauthorized' do
+        expect { post "/api/v1/accounts/#{account.id}/dashboard_apps", params: payload }.to change(CustomFilter, :count).by(0)
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+
+    context 'when it is an authenticated user' do
+      let(:user) { create(:user, account: account) }
+
+      it 'creates the dashboard app' do
+        expect do
+          post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
+                                                                params: payload
+        end.to change(DashboardApp, :count).by(1)
+
+        expect(response).to have_http_status(:success)
+        json_response = JSON.parse(response.body)
+        expect(json_response['title']).to eq 'CRM Dashboard'
+        expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
+        expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
+      end
+
+      it 'does not create the dashboard app if invalid URL' do
+        expect do
+          post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
+                                                                params: invalid_url_payload
+        end.to change(DashboardApp, :count).by(0)
+
+        expect(response).to have_http_status(:unprocessable_entity)
+        json_response = JSON.parse(response.body)
+        expect(json_response['message']).to eq 'Content : Invalid data'
+      end
+
+      it 'does not create the dashboard app if invalid type' do
+        expect do
+          post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
+                                                                params: invalid_type_payload
+        end.to change(DashboardApp, :count).by(0)
+
+        expect(response).to have_http_status(:unprocessable_entity)
+      end
+    end
+  end
+
+  describe 'PATCH /api/v1/accounts/{account.id}/dashboard_apps/:id' do
+    let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } }
+    let(:user) { create(:user, account: account) }
+    let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
+
+    context 'when it is an unauthenticated user' do
+      it 'returns unauthorized' do
+        put "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
+            params: payload
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+
+    context 'when it is an authenticated user' do
+      it 'updates the dashboard app' do
+        patch "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
+              headers: user.create_new_auth_token,
+              params: payload,
+              as: :json
+
+        expect(response).to have_http_status(:success)
+        json_response = JSON.parse(response.body)
+        expect(dashboard_app.reload.title).to eq('CRM Dashboard')
+        expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
+        expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
+      end
+    end
+  end
+
+  describe 'DELETE /api/v1/accounts/{account.id}/dashboard_apps/:id' do
+    let(:user) { create(:user, account: account) }
+    let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
+
+    context 'when it is an unauthenticated user' do
+      it 'returns unauthorized' do
+        delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}"
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+
+    context 'when it is an authenticated admin user' do
+      it 'deletes dashboard app' do
+        delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
+               headers: user.create_new_auth_token,
+               as: :json
+        expect(response).to have_http_status(:no_content)
+        expect(user.dashboard_apps.count).to be 0
+      end
+    end
+  end
+end
diff --git a/spec/factories/dashboard_app.rb b/spec/factories/dashboard_app.rb
new file mode 100644
index 000000000..559e966b6
--- /dev/null
+++ b/spec/factories/dashboard_app.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :dashboard_app do
+    sequence(:title) { |n| "Dashboard App #{n}" }
+    content { [{ type: 'frame', url: 'https://chatwoot.com' }] }
+    user
+    account
+  end
+end