Issue:
Bug: setup roles table



Setup `roles` table  along with db functions to support custom roles.

Pull request description:
## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
Setup roles table to support custom roles.

### Additional Changes

- [ ] This PR modifies the API contract
- [x] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context



## How did you test it?
Will be dependent on one core PR; checked table schema locally by running the migrations.
After core functions micro PR containing Apis to interact with DB this can be tested in other environments as well.


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable


Testing:
Will be dependent on one core PR; checked table schema locally by running the migrations.
After core functions micro PR containing Apis to interact with DB this can be tested in other environments as well.

Patch:
diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs
index 7ca65ac4b76..4ca5b4c986c 100644
--- a/crates/diesel_models/src/enums.rs
+++ b/crates/diesel_models/src/enums.rs
@@ -17,7 +17,8 @@ pub mod diesel_exports {
         DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus,
         DbRefundStatus as RefundStatus, DbRefundType as RefundType,
         DbRequestIncrementalAuthorization as RequestIncrementalAuthorization,
-        DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbUserStatus as UserStatus,
+        DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind,
+        DbUserStatus as UserStatus,
     };
 }
 pub use common_enums::*;
@@ -500,3 +501,53 @@ pub enum DashboardMetadata {
     IsMultipleConfiguration,
     IsChangePasswordRequired,
 }
+
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    PartialEq,
+    serde::Deserialize,
+    serde::Serialize,
+    strum::Display,
+    strum::EnumString,
+    frunk::LabelledGeneric,
+)]
+#[diesel_enum(storage_type = "db_enum")]
+#[serde(rename_all = "snake_case")]
+#[strum(serialize_all = "snake_case")]
+pub enum RoleScope {
+    Merchant,
+    Organization,
+}
+
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    PartialEq,
+    serde::Serialize,
+    serde::Deserialize,
+    strum::Display,
+    strum::EnumString,
+    frunk::LabelledGeneric,
+)]
+#[diesel_enum(storage_type = "text")]
+#[serde(rename_all = "snake_case")]
+#[strum(serialize_all = "snake_case")]
+pub enum PermissionGroup {
+    OperationsView,
+    OperationsManage,
+    ConnectorsView,
+    ConnectorsManage,
+    WorkflowsView,
+    WorkflowsManage,
+    AnalyticsView,
+    UsersView,
+    UsersManage,
+    MerchantDetailsView,
+    MerchantDetailsManage,
+    OrganizationManage,
+}
diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs
index 82b1e29ee83..f7678793bc2 100644
--- a/crates/diesel_models/src/lib.rs
+++ b/crates/diesel_models/src/lib.rs
@@ -39,6 +39,7 @@ pub mod process_tracker;
 pub mod query;
 pub mod refund;
 pub mod reverse_lookup;
+pub mod role;
 pub mod routing_algorithm;
 #[allow(unused_qualifications)]
 pub mod schema;
diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs
index 3a0a008b76b..c395ae3df92 100644
--- a/crates/diesel_models/src/query.rs
+++ b/crates/diesel_models/src/query.rs
@@ -32,6 +32,7 @@ pub mod payouts;
 pub mod process_tracker;
 pub mod refund;
 pub mod reverse_lookup;
+pub mod role;
 pub mod routing_algorithm;
 pub mod user;
 pub mod user_role;
diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs
new file mode 100644
index 00000000000..b8704aebbd0
--- /dev/null
+++ b/crates/diesel_models/src/query/role.rs
@@ -0,0 +1,68 @@
+use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods};
+use router_env::tracing::{self, instrument};
+
+use crate::{
+    enums::RoleScope, query::generics, role::*, schema::roles::dsl, PgPooledConn, StorageResult,
+};
+
+impl RoleNew {
+    #[instrument(skip(conn))]
+    pub async fn insert(self, conn: &PgPooledConn) -> StorageResult<Role> {
+        generics::generic_insert(conn, self).await
+    }
+}
+
+impl Role {
+    pub async fn find_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult<Self> {
+        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
+            conn,
+            dsl::role_id.eq(role_id.to_owned()),
+        )
+        .await
+    }
+
+    pub async fn update_by_role_id(
+        conn: &PgPooledConn,
+        role_id: &str,
+        role_update: RoleUpdate,
+    ) -> StorageResult<Self> {
+        generics::generic_update_with_unique_predicate_get_result::<
+            <Self as HasTable>::Table,
+            _,
+            _,
+            _,
+        >(
+            conn,
+            dsl::role_id.eq(role_id.to_owned()),
+            RoleUpdateInternal::from(role_update),
+        )
+        .await
+    }
+
+    pub async fn delete_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult<Self> {
+        generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(
+            conn,
+            dsl::role_id.eq(role_id.to_owned()),
+        )
+        .await
+    }
+
+    pub async fn list_roles(
+        conn: &PgPooledConn,
+        merchant_id: &str,
+        org_id: &str,
+    ) -> StorageResult<Vec<Self>> {
+        let predicate = dsl::merchant_id.eq(merchant_id.to_owned()).or(dsl::org_id
+            .eq(org_id.to_owned())
+            .and(dsl::scope.eq(RoleScope::Organization)));
+
+        generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
+            conn,
+            predicate,
+            None,
+            None,
+            Some(dsl::last_modified_at.asc()),
+        )
+        .await
+    }
+}
diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs
new file mode 100644
index 00000000000..6b21b1461cd
--- /dev/null
+++ b/crates/diesel_models/src/role.rs
@@ -0,0 +1,83 @@
+use diesel::{AsChangeset, Identifiable, Insertable, Queryable};
+use time::PrimitiveDateTime;
+
+use crate::{enums, schema::roles};
+
+#[derive(Clone, Debug, Identifiable, Queryable)]
+#[diesel(table_name = roles)]
+pub struct Role {
+    pub id: i32,
+    pub role_name: String,
+    pub role_id: String,
+    pub merchant_id: String,
+    pub org_id: String,
+    #[diesel(deserialize_as = super::DieselArray<enums::PermissionGroup>)]
+    pub groups: Vec<enums::PermissionGroup>,
+    pub scope: enums::RoleScope,
+    pub created_at: PrimitiveDateTime,
+    pub created_by: String,
+    pub last_modified_at: PrimitiveDateTime,
+    pub last_modified_by: String,
+}
+
+#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
+#[diesel(table_name = roles)]
+pub struct RoleNew {
+    pub role_name: String,
+    pub role_id: String,
+    pub merchant_id: String,
+    pub org_id: String,
+    #[diesel(deserialize_as = super::DieselArray<enums::PermissionGroup>)]
+    pub groups: Vec<enums::PermissionGroup>,
+    pub scope: enums::RoleScope,
+    pub created_at: PrimitiveDateTime,
+    pub created_by: String,
+    pub last_modified_at: PrimitiveDateTime,
+    pub last_modified_by: String,
+}
+
+#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
+#[diesel(table_name = roles)]
+pub struct RoleUpdateInternal {
+    groups: Option<Vec<enums::PermissionGroup>>,
+    role_name: Option<String>,
+    last_modified_by: String,
+    last_modified_at: PrimitiveDateTime,
+}
+
+pub enum RoleUpdate {
+    UpdateGroup {
+        groups: Vec<enums::PermissionGroup>,
+        last_modified_by: String,
+    },
+    UpdateRoleName {
+        role_name: String,
+        last_modified_by: String,
+    },
+}
+
+impl From<RoleUpdate> for RoleUpdateInternal {
+    fn from(value: RoleUpdate) -> Self {
+        let last_modified_at = common_utils::date_time::now();
+        match value {
+            RoleUpdate::UpdateGroup {
+                groups,
+                last_modified_by,
+            } => Self {
+                groups: Some(groups),
+                role_name: None,
+                last_modified_at,
+                last_modified_by,
+            },
+            RoleUpdate::UpdateRoleName {
+                role_name,
+                last_modified_by,
+            } => Self {
+                groups: None,
+                role_name: Some(role_name),
+                last_modified_at,
+                last_modified_by,
+            },
+        }
+    }
+}
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index 6c28677432b..2cbcb52fb44 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -994,6 +994,31 @@ diesel::table! {
     }
 }
 
+diesel::table! {
+    use diesel::sql_types::*;
+    use crate::enums::diesel_exports::*;
+
+    roles (id) {
+        id -> Int4,
+        #[max_length = 64]
+        role_name -> Varchar,
+        #[max_length = 64]
+        role_id -> Varchar,
+        #[max_length = 64]
+        merchant_id -> Varchar,
+        #[max_length = 64]
+        org_id -> Varchar,
+        groups -> Array<Nullable<Text>>,
+        scope -> RoleScope,
+        created_at -> Timestamp,
+        #[max_length = 64]
+        created_by -> Varchar,
+        last_modified_at -> Timestamp,
+        #[max_length = 64]
+        last_modified_by -> Varchar,
+    }
+}
+
 diesel::table! {
     use diesel::sql_types::*;
     use crate::enums::diesel_exports::*;
@@ -1095,6 +1120,7 @@ diesel::allow_tables_to_appear_in_same_query!(
     process_tracker,
     refund,
     reverse_lookup,
+    roles,
     routing_algorithm,
     user_roles,
     users,
diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs
index 54900177246..d385d94a5de 100644
--- a/crates/router/src/db.rs
+++ b/crates/router/src/db.rs
@@ -31,6 +31,7 @@ pub mod payout_attempt;
 pub mod payouts;
 pub mod refund;
 pub mod reverse_lookup;
+pub mod role;
 pub mod routing_algorithm;
 pub mod user;
 pub mod user_role;
@@ -111,6 +112,7 @@ pub trait StorageInterface:
     + authorization::AuthorizationInterface
     + user::sample_data::BatchSampleDataInterface
     + health_check::HealthCheckDbInterface
+    + role::RoleInterface
     + 'static
 {
     fn get_scheduler_db(&self) -> Box<dyn scheduler::SchedulerInterface>;
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 0078f030c5a..0897deb87e5 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -24,6 +24,7 @@ use time::PrimitiveDateTime;
 
 use super::{
     dashboard_metadata::DashboardMetadataInterface,
+    role::RoleInterface,
     user::{sample_data::BatchSampleDataInterface, UserInterface},
     user_role::UserRoleInterface,
 };
@@ -2256,3 +2257,45 @@ impl HealthCheckDbInterface for KafkaStore {
         self.diesel_store.health_check_db().await
     }
 }
+
+#[async_trait::async_trait]
+impl RoleInterface for KafkaStore {
+    async fn insert_role(
+        &self,
+        role: storage::RoleNew,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        self.diesel_store.insert_role(role).await
+    }
+
+    async fn find_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        self.diesel_store.find_role_by_role_id(role_id).await
+    }
+
+    async fn update_role_by_role_id(
+        &self,
+        role_id: &str,
+        role_update: storage::RoleUpdate,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        self.diesel_store
+            .update_role_by_role_id(role_id, role_update)
+            .await
+    }
+
+    async fn delete_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        self.diesel_store.delete_role_by_role_id(role_id).await
+    }
+
+    async fn list_all_roles(
+        &self,
+        merchant_id: &str,
+        org_id: &str,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
+        self.diesel_store.list_all_roles(merchant_id, org_id).await
+    }
+}
diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs
new file mode 100644
index 00000000000..da2abf15ff7
--- /dev/null
+++ b/crates/router/src/db/role.rs
@@ -0,0 +1,236 @@
+use diesel_models::role as storage;
+use error_stack::{IntoReport, ResultExt};
+
+use super::MockDb;
+use crate::{
+    connection,
+    core::errors::{self, CustomResult},
+    services::Store,
+};
+
+#[async_trait::async_trait]
+pub trait RoleInterface {
+    async fn insert_role(
+        &self,
+        role: storage::RoleNew,
+    ) -> CustomResult<storage::Role, errors::StorageError>;
+
+    async fn find_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError>;
+
+    async fn update_role_by_role_id(
+        &self,
+        role_id: &str,
+        role_update: storage::RoleUpdate,
+    ) -> CustomResult<storage::Role, errors::StorageError>;
+
+    async fn delete_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError>;
+
+    async fn list_all_roles(
+        &self,
+        merchant_id: &str,
+        org_id: &str,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError>;
+}
+
+#[async_trait::async_trait]
+impl RoleInterface for Store {
+    async fn insert_role(
+        &self,
+        role: storage::RoleNew,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        role.insert(&conn).await.map_err(Into::into).into_report()
+    }
+
+    async fn find_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::Role::find_by_role_id(&conn, role_id)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn update_role_by_role_id(
+        &self,
+        role_id: &str,
+        role_update: storage::RoleUpdate,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::Role::update_by_role_id(&conn, role_id, role_update)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn delete_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::Role::delete_by_role_id(&conn, role_id)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn list_all_roles(
+        &self,
+        merchant_id: &str,
+        org_id: &str,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::Role::list_roles(&conn, merchant_id, org_id)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+}
+
+#[async_trait::async_trait]
+impl RoleInterface for MockDb {
+    async fn insert_role(
+        &self,
+        role: storage::RoleNew,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let mut roles = self.roles.lock().await;
+        if roles
+            .iter()
+            .any(|role_inner| role_inner.role_id == role.role_id)
+        {
+            Err(errors::StorageError::DuplicateValue {
+                entity: "role_id",
+                key: None,
+            })?
+        }
+        let role = storage::Role {
+            id: roles
+                .len()
+                .try_into()
+                .into_report()
+                .change_context(errors::StorageError::MockDbError)?,
+            role_name: role.role_name,
+            role_id: role.role_id,
+            merchant_id: role.merchant_id,
+            org_id: role.org_id,
+            groups: role.groups,
+            scope: role.scope,
+            created_by: role.created_by,
+            created_at: role.created_at,
+            last_modified_at: role.last_modified_at,
+            last_modified_by: role.last_modified_by,
+        };
+        roles.push(role.clone());
+        Ok(role)
+    }
+
+    async fn find_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let roles = self.roles.lock().await;
+        roles
+            .iter()
+            .find(|role| role.role_id == role_id)
+            .cloned()
+            .ok_or(
+                errors::StorageError::ValueNotFound(format!(
+                    "No role available role_id  = {role_id}"
+                ))
+                .into(),
+            )
+    }
+
+    async fn update_role_by_role_id(
+        &self,
+        role_id: &str,
+        role_update: storage::RoleUpdate,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let mut roles = self.roles.lock().await;
+        let last_modified_at = common_utils::date_time::now();
+
+        roles
+            .iter_mut()
+            .find(|role| role.role_id == role_id)
+            .map(|role| {
+                *role = match role_update {
+                    storage::RoleUpdate::UpdateGroup {
+                        groups,
+                        last_modified_by,
+                    } => storage::Role {
+                        groups,
+                        last_modified_by,
+                        last_modified_at,
+                        ..role.to_owned()
+                    },
+                    storage::RoleUpdate::UpdateRoleName {
+                        role_name,
+                        last_modified_by,
+                    } => storage::Role {
+                        role_name,
+                        last_modified_at,
+                        last_modified_by,
+                        ..role.to_owned()
+                    },
+                };
+                role.to_owned()
+            })
+            .ok_or(
+                errors::StorageError::ValueNotFound(format!(
+                    "No role available for role_id = {role_id}"
+                ))
+                .into(),
+            )
+    }
+
+    async fn delete_role_by_role_id(
+        &self,
+        role_id: &str,
+    ) -> CustomResult<storage::Role, errors::StorageError> {
+        let mut roles = self.roles.lock().await;
+        let role_index = roles
+            .iter()
+            .position(|role| role.role_id == role_id)
+            .ok_or(errors::StorageError::ValueNotFound(format!(
+                "No role available for role_id = {role_id}"
+            )))?;
+
+        Ok(roles.remove(role_index))
+    }
+
+    async fn list_all_roles(
+        &self,
+        merchant_id: &str,
+        org_id: &str,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
+        let roles = self.roles.lock().await;
+
+        let roles_list: Vec<_> = roles
+            .iter()
+            .filter(|role| {
+                role.merchant_id == merchant_id
+                    || (role.org_id == org_id
+                        && role.scope == diesel_models::enums::RoleScope::Organization)
+            })
+            .cloned()
+            .collect();
+
+        if roles_list.is_empty() {
+            return Err(errors::StorageError::ValueNotFound(format!(
+                "No role found for merchant id = {} and org_id = {}",
+                merchant_id, org_id
+            ))
+            .into());
+        }
+
+        Ok(roles_list)
+    }
+}
diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs
index b93cbbbbba9..01decc89c4c 100644
--- a/crates/router/src/types/storage.rs
+++ b/crates/router/src/types/storage.rs
@@ -31,6 +31,7 @@ pub mod payout_attempt;
 pub mod payouts;
 pub mod refund;
 pub mod reverse_lookup;
+pub mod role;
 pub mod routing_algorithm;
 pub mod user;
 pub mod user_role;
@@ -51,7 +52,8 @@ pub use self::{
     dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*,
     gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
     merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*,
-    process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*,
+    process_tracker::*, refund::*, reverse_lookup::*, role::*, routing_algorithm::*, user::*,
+    user_role::*,
 };
 use crate::types::api::routing;
 
diff --git a/crates/router/src/types/storage/role.rs b/crates/router/src/types/storage/role.rs
new file mode 100644
index 00000000000..4a831762a4e
--- /dev/null
+++ b/crates/router/src/types/storage/role.rs
@@ -0,0 +1 @@
+pub use diesel_models::role::*;
diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs
index a6ba763bd91..a1518cea431 100644
--- a/crates/storage_impl/src/mock_db.rs
+++ b/crates/storage_impl/src/mock_db.rs
@@ -45,6 +45,7 @@ pub struct MockDb {
     pub user_roles: Arc<Mutex<Vec<store::user_role::UserRole>>>,
     pub authorizations: Arc<Mutex<Vec<store::authorization::Authorization>>>,
     pub dashboard_metadata: Arc<Mutex<Vec<store::user::dashboard_metadata::DashboardMetadata>>>,
+    pub roles: Arc<Mutex<Vec<store::role::Role>>>,
 }
 
 impl MockDb {
@@ -82,6 +83,7 @@ impl MockDb {
             user_roles: Default::default(),
             authorizations: Default::default(),
             dashboard_metadata: Default::default(),
+            roles: Default::default(),
         })
     }
 }
diff --git a/migrations/2024-02-14-092225_create_roles_table/down.sql b/migrations/2024-02-14-092225_create_roles_table/down.sql
new file mode 100644
index 00000000000..c8b1c055f7d
--- /dev/null
+++ b/migrations/2024-02-14-092225_create_roles_table/down.sql
@@ -0,0 +1,6 @@
+-- This file should undo anything in `up.sql`
+DROP INDEX IF EXISTS role_id_index;
+DROP INDEX IF EXISTS roles_merchant_org_index;
+
+DROP TABLE IF EXISTS roles;
+DROP TYPE "RoleScope";
\ No newline at end of file
diff --git a/migrations/2024-02-14-092225_create_roles_table/up.sql b/migrations/2024-02-14-092225_create_roles_table/up.sql
new file mode 100644
index 00000000000..c33c133a078
--- /dev/null
+++ b/migrations/2024-02-14-092225_create_roles_table/up.sql
@@ -0,0 +1,19 @@
+-- Your SQL goes here
+CREATE TYPE "RoleScope" AS ENUM ('merchant','organization');
+
+CREATE TABLE IF NOT EXISTS roles (
+    id SERIAL PRIMARY KEY,
+    role_name VARCHAR(64) NOT NULL,
+    role_id VARCHAR(64) NOT NULL UNIQUE,
+    merchant_id VARCHAR(64) NOT NULL,
+    org_id VARCHAR(64) NOT NULL,
+    groups TEXT[] NOT NULL,
+    scope "RoleScope" NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT now(),
+    created_by VARCHAR(64) NOT NULL,
+    last_modified_at TIMESTAMP NOT NULL DEFAULT now(),
+    last_modified_by VARCHAR(64) NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS role_id_index ON roles (role_id);
+CREATE INDEX roles_merchant_org_index ON roles (merchant_id, org_id);
\ No newline at end of file