Issue:
Bug: feat(themes): APIs for theme management



Backend needs to provide APIs to create and manage themes. Four APIs are planned
1. Create theme
2. Update theme data
3. Delete theme
4. Upload an asset to theme

![Themes APIs](https://github.com/user-attachments/assets/71b9e349-12b7-45db-8589-7babbb8ea4e3)


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
<!-- Describe your changes in detail -->
This PR adds APIs to manage themes.
These are the following APIs
1. Create Theme
2. Delete Theme
3. Update Theme
4. Upload Asset
5. Find Theme by Theme ID
6. Find Theme by Lineage

### Additional Changes

- [x] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [x] 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`
7. `crates/router/src/configs`
8. `loadtest/config`
-->
todo!()

## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Closes #6553 

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
1. Create Theme
    ```
    curl --location 'http://localhost:8080/user/theme' \
    --header 'Content-Type: application/json' \
    --header 'api-key: ••••••' \
    --data '{
        "lineage": {
            "entity_type": "profile",
            "tenant_id": "t1",
            "org_id": "o1",
            "merchant_id": "m1",
            "profile_id": "p1"
        },
        "theme_name": "Test",
        "theme_data": {
            "settings": {
                "colors": {
                    "primary": "#3498db",
                    "sidebar": "#2ecc71"
                },
                "buttons": {
                    "primary": {
                        "hoverBackgroundColor": "#27ae60"
                    }
                }
            }
        }
    }'
    ```
2. Delete Theme
    ```
    curl --location --request DELETE 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63?entity_type=profile&tenant_id=t1&org_id=o1&merchant_id=m1&profile_id=p1' \
    --header 'api-key: ••••••'
    ```
3. Update Theme
    ```
    curl --location --request PUT 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63' \
    --header 'Content-Type: application/json' \
    --header 'api-key: test_admin' \
    --data '{
        "lineage": {
            "entity_type": "profile",
            "tenant_id": "t1",
            "org_id": "o1",
            "merchant_id": "m1",
            "profile_id": "p1"
        },
        "theme_data": {
            "settings": {
                "colors": {
                    "primary": "#3498xb",
                    "sidebar": "#2ecc71"
                },
                "buttons": {
                    "primary": {
                        "hoverBackgroundColor": "#27ae60"
                    }
                }
            }
        }
    }'
    ```
4. Upload Asset
    ```
    curl --location 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63?entity_type=profile&tenant_id=t1&org_id=o1&merchant_id=m1&profile_id=p1' \
    --header 'api-key: ••••••' \
    --form 'asset_name="logo.png"' \
    --form 'asset_data=@"path to file"'
    ```
5. Get Theme by Theme ID
    ```
    curl --location 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63' \
    --header 'api-key: ••••••'
    ```
6. Get Theme by Lineage
    ```
    curl --location 'http://localhost:8080/user/theme?entity_type=merchant&tenant_id=t1&org_id=o1&merchant_id=m1&profile_id=p1' \
    --header 'api-key: ••••••'
    ```

Responses
- Delete Theme and Upload Asset: 200 OK
- Everything else
    ```
    {
        "theme_id": "087c79dc-640c-4a2a-8552-6fa4e868cc63",
        "theme_name": "Test",
        "entity_type": "profile",
        "tenant_id": "t1",
        "org_id": "o1",
        "merchant_id": "m1",
        "profile_id": "p1",
        "theme_data": {
            "settings": {
                "colors": {
                    "primary": "#3498db",
                    "sidebar": "#2ecc71"
                },
                "typography": null,
                "buttons": {
                    "primary": {
                        "backgroundColor": null,
                        "textColor": null,
                        "hoverBackgroundColor": "#27ae60"
                    },
                    "secondary": null
                },
                "borders": null,
                "spacing": null
            },
            "urls": null
        }
    }
    ```



## 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


Testing:

1. Create Theme
    ```
    curl --location 'http://localhost:8080/user/theme' \
    --header 'Content-Type: application/json' \
    --header 'api-key: ••••••' \
    --data '{
        "lineage": {
            "entity_type": "profile",
            "tenant_id": "t1",
            "org_id": "o1",
            "merchant_id": "m1",
            "profile_id": "p1"
        },
        "theme_name": "Test",
        "theme_data": {
            "settings": {
                "colors": {
                    "primary": "#3498db",
                    "sidebar": "#2ecc71"
                },
                "buttons": {
                    "primary": {
                        "hoverBackgroundColor": "#27ae60"
                    }
                }
            }
        }
    }'
    ```
2. Delete Theme
    ```
    curl --location --request DELETE 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63?entity_type=profile&tenant_id=t1&org_id=o1&merchant_id=m1&profile_id=p1' \
    --header 'api-key: ••••••'
    ```
3. Update Theme
    ```
    curl --location --request PUT 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63' \
    --header 'Content-Type: application/json' \
    --header 'api-key: test_admin' \
    --data '{
        "lineage": {
            "entity_type": "profile",
            "tenant_id": "t1",
            "org_id": "o1",
            "merchant_id": "m1",
            "profile_id": "p1"
        },
        "theme_data": {
            "settings": {
                "colors": {
                    "primary": "#3498xb",
                    "sidebar": "#2ecc71"
                },
                "buttons": {
                    "primary": {
                        "hoverBackgroundColor": "#27ae60"
                    }
                }
            }
        }
    }'
    ```
4. Upload Asset
    ```
    curl --location 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63?entity_type=profile&tenant_id=t1&org_id=o1&merchant_id=m1&profile_id=p1' \
    --header 'api-key: ••••••' \
    --form 'asset_name="logo.png"' \
    --form 'asset_data=@"path to file"'
    ```
5. Get Theme by Theme ID
    ```
    curl --location 'http://localhost:8080/user/theme/087c79dc-640c-4a2a-8552-6fa4e868cc63' \
    --header 'api-key: ••••••'
    ```
6. Get Theme by Lineage
    ```
    curl --location 'http://localhost:8080/user/theme?entity_type=merchant&tenant_id=t1&org_id=o1&merchant_id=m1&profile_id=p1' \
    --header 'api-key: ••••••'
    ```

Responses
- Delete Theme and Upload Asset: 200 OK
- Everything else
    ```
    {
        "theme_id": "087c79dc-640c-4a2a-8552-6fa4e868cc63",
        "theme_name": "Test",
        "entity_type": "profile",
        "tenant_id": "t1",
        "org_id": "o1",
        "merchant_id": "m1",
        "profile_id": "p1",
        "theme_data": {
            "settings": {
                "colors": {
                    "primary": "#3498db",
                    "sidebar": "#2ecc71"
                },
                "typography": null,
                "buttons": {
                    "primary": {
                        "backgroundColor": null,
                        "textColor": null,
                        "hoverBackgroundColor": "#27ae60"
                    },
                    "secondary": null
                },
                "borders": null,
                "spacing": null
            },
            "urls": null
        }
    }
    ```

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4118ea29c2d..9b0263f99dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -455,6 +455,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 name = "api_models"
 version = "0.1.0"
 dependencies = [
+ "actix-multipart",
  "actix-web",
  "cards",
  "common_enums",
diff --git a/config/config.example.toml b/config/config.example.toml
index 03ebedf0d35..54a3ab3827b 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -795,3 +795,6 @@ connector_list = "stripe,adyen,cybersource" # Supported connectors for network t
 host = "localhost" # Client Host
 port = 7000        # Client Port
 service = "dynamo" # Service name
+
+[theme_storage]
+file_storage_backend = "file_system" # Theme storage backend to be used
diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml
index fa0c0484a77..5cadc66ddfc 100644
--- a/config/deployments/env_specific.toml
+++ b/config/deployments/env_specific.toml
@@ -327,3 +327,10 @@ check_token_status_url= ""    # base url to check token status from token servic
 host = "localhost" # Client Host
 port = 7000        # Client Port
 service = "dynamo" # Service name
+
+[theme_storage]
+file_storage_backend = "aws_s3" # Theme storage backend to be used
+
+[theme_storage.aws_s3]
+region = "bucket_region" # AWS region where the S3 bucket for theme storage is located
+bucket_name = "bucket"   # AWS S3 bucket name for theme storage
diff --git a/config/development.toml b/config/development.toml
index d8251cfce7b..eef44648c6c 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -793,3 +793,6 @@ connector_list = "cybersource"
 [grpc_client.dynamic_routing_client] 
 host = "localhost" 
 port = 7000       
+
+[theme_storage]
+file_storage_backend = "file_system"
diff --git a/config/docker_compose.toml b/config/docker_compose.toml
index 976a2fa2a42..f38da4183b1 100644
--- a/config/docker_compose.toml
+++ b/config/docker_compose.toml
@@ -691,3 +691,6 @@ prod_intent_recipient_email = "business@example.com" # Recipient email for prod
 [email.aws_ses]
 email_role_arn = ""        # The amazon resource name ( arn ) of the role which has permission to send emails
 sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
+
+[theme_storage]
+file_storage_backend = "file_system" # Theme storage backend to be used
diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml
index a5d702e26fb..eb706dc913c 100644
--- a/crates/api_models/Cargo.toml
+++ b/crates/api_models/Cargo.toml
@@ -8,7 +8,7 @@ readme = "README.md"
 license.workspace = true
 
 [features]
-errors = ["dep:actix-web", "dep:reqwest"]
+errors = ["dep:reqwest"]
 dummy_connector = ["euclid/dummy_connector", "common_enums/dummy_connector"]
 detailed_errors = []
 payouts = ["common_enums/payouts"]
@@ -23,7 +23,8 @@ payment_methods_v2 = ["common_utils/payment_methods_v2"]
 dynamic_routing = []
 
 [dependencies]
-actix-web = { version = "4.5.1", optional = true }
+actix-multipart = "0.6.1"
+actix-web = "4.5.1"
 error-stack = "0.4.1"
 indexmap = "2.3.0"
 mime = "0.3.17"
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index 4dc2a1a301a..3260ef02645 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -10,6 +10,7 @@ use crate::user::{
     dashboard_metadata::{
         GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
     },
+    theme::{CreateThemeRequest, GetThemeResponse, UpdateThemeRequest, UploadFileRequest},
     AcceptInviteFromEmailRequest, AuthSelectRequest, AuthorizeResponse, BeginTotpResponse,
     ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
     CreateUserAuthenticationMethodRequest, ForgotPasswordRequest, GetSsoAuthUrlRequest,
@@ -74,7 +75,11 @@ common_utils::impl_api_event_type!(
         UpdateUserAuthenticationMethodRequest,
         GetSsoAuthUrlRequest,
         SsoSignInRequest,
-        AuthSelectRequest
+        AuthSelectRequest,
+        GetThemeResponse,
+        UploadFileRequest,
+        CreateThemeRequest,
+        UpdateThemeRequest
     )
 );
 
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 089426c68ba..5dc3f544297 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -8,6 +8,7 @@ use crate::user_role::UserStatus;
 pub mod dashboard_metadata;
 #[cfg(feature = "dummy_connector")]
 pub mod sample_data;
+pub mod theme;
 
 #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)]
 pub struct SignUpWithMerchantIdRequest {
diff --git a/crates/api_models/src/user/theme.rs b/crates/api_models/src/user/theme.rs
new file mode 100644
index 00000000000..23218c4a163
--- /dev/null
+++ b/crates/api_models/src/user/theme.rs
@@ -0,0 +1,125 @@
+use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm};
+use common_enums::EntityType;
+use common_utils::{id_type, types::theme::ThemeLineage};
+use masking::Secret;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize)]
+pub struct GetThemeResponse {
+    pub theme_id: String,
+    pub theme_name: String,
+    pub entity_type: EntityType,
+    pub tenant_id: id_type::TenantId,
+    pub org_id: Option<id_type::OrganizationId>,
+    pub merchant_id: Option<id_type::MerchantId>,
+    pub profile_id: Option<id_type::ProfileId>,
+    pub theme_data: ThemeData,
+}
+
+#[derive(Debug, MultipartForm)]
+pub struct UploadFileAssetData {
+    pub asset_name: Text<String>,
+    #[multipart(limit = "10MB")]
+    pub asset_data: Bytes,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct UploadFileRequest {
+    pub lineage: ThemeLineage,
+    pub asset_name: String,
+    pub asset_data: Secret<Vec<u8>>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct CreateThemeRequest {
+    pub lineage: ThemeLineage,
+    pub theme_name: String,
+    pub theme_data: ThemeData,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct UpdateThemeRequest {
+    pub lineage: ThemeLineage,
+    pub theme_data: ThemeData,
+}
+
+// All the below structs are for the theme.json file,
+// which will be used by frontend to style the dashboard.
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ThemeData {
+    settings: Settings,
+    urls: Option<Urls>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Settings {
+    colors: Colors,
+    typography: Option<Typography>,
+    buttons: Buttons,
+    borders: Option<Borders>,
+    spacing: Option<Spacing>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Colors {
+    primary: String,
+    sidebar: String,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Typography {
+    font_family: Option<String>,
+    font_size: Option<String>,
+    heading_font_size: Option<String>,
+    text_color: Option<String>,
+    link_color: Option<String>,
+    link_hover_color: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Buttons {
+    primary: PrimaryButton,
+    secondary: Option<SecondaryButton>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct PrimaryButton {
+    background_color: Option<String>,
+    text_color: Option<String>,
+    hover_background_color: String,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct SecondaryButton {
+    background_color: Option<String>,
+    text_color: Option<String>,
+    hover_background_color: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Borders {
+    default_radius: Option<String>,
+    border_color: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Spacing {
+    padding: Option<String>,
+    margin: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct Urls {
+    favicon_url: Option<String>,
+    logo_url: Option<String>,
+}
diff --git a/crates/common_utils/src/types/theme.rs b/crates/common_utils/src/types/theme.rs
index 9ad9206acce..239cffc40d4 100644
--- a/crates/common_utils/src/types/theme.rs
+++ b/crates/common_utils/src/types/theme.rs
@@ -1,8 +1,14 @@
-use crate::id_type;
+use common_enums::EntityType;
+
+use crate::{
+    events::{ApiEventMetric, ApiEventsType},
+    id_type, impl_api_event_type,
+};
 
 /// Enum for having all the required lineage for every level.
 /// Currently being used for theme related APIs and queries.
-#[derive(Debug)]
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+#[serde(tag = "entity_type", rename_all = "snake_case")]
 pub enum ThemeLineage {
     // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType
     // /// Tenant lineage variant
@@ -38,3 +44,52 @@ pub enum ThemeLineage {
         profile_id: id_type::ProfileId,
     },
 }
+
+impl_api_event_type!(Miscellaneous, (ThemeLineage));
+
+impl ThemeLineage {
+    /// Get the entity_type from the lineage
+    pub fn entity_type(&self) -> EntityType {
+        match self {
+            Self::Organization { .. } => EntityType::Organization,
+            Self::Merchant { .. } => EntityType::Merchant,
+            Self::Profile { .. } => EntityType::Profile,
+        }
+    }
+
+    /// Get the tenant_id from the lineage
+    pub fn tenant_id(&self) -> &id_type::TenantId {
+        match self {
+            Self::Organization { tenant_id, .. }
+            | Self::Merchant { tenant_id, .. }
+            | Self::Profile { tenant_id, .. } => tenant_id,
+        }
+    }
+
+    /// Get the org_id from the lineage
+    pub fn org_id(&self) -> Option<&id_type::OrganizationId> {
+        match self {
+            Self::Organization { org_id, .. }
+            | Self::Merchant { org_id, .. }
+            | Self::Profile { org_id, .. } => Some(org_id),
+        }
+    }
+
+    /// Get the merchant_id from the lineage
+    pub fn merchant_id(&self) -> Option<&id_type::MerchantId> {
+        match self {
+            Self::Organization { .. } => None,
+            Self::Merchant { merchant_id, .. } | Self::Profile { merchant_id, .. } => {
+                Some(merchant_id)
+            }
+        }
+    }
+
+    /// Get the profile_id from the lineage
+    pub fn profile_id(&self) -> Option<&id_type::ProfileId> {
+        match self {
+            Self::Organization { .. } | Self::Merchant { .. } => None,
+            Self::Profile { profile_id, .. } => Some(profile_id),
+        }
+    }
+}
diff --git a/crates/diesel_models/src/query/user/theme.rs b/crates/diesel_models/src/query/user/theme.rs
index 78fd5025ef9..a26e401ffbb 100644
--- a/crates/diesel_models/src/query/user/theme.rs
+++ b/crates/diesel_models/src/query/user/theme.rs
@@ -69,6 +69,14 @@ impl Theme {
         }
     }
 
+    pub async fn find_by_theme_id(conn: &PgPooledConn, theme_id: String) -> StorageResult<Self> {
+        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
+            conn,
+            dsl::theme_id.eq(theme_id),
+        )
+        .await
+    }
+
     pub async fn find_by_lineage(
         conn: &PgPooledConn,
         lineage: ThemeLineage,
diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs
index 9841e21443c..e43a182126b 100644
--- a/crates/diesel_models/src/user/theme.rs
+++ b/crates/diesel_models/src/user/theme.rs
@@ -1,5 +1,5 @@
 use common_enums::EntityType;
-use common_utils::id_type;
+use common_utils::{date_time, id_type, types::theme::ThemeLineage};
 use diesel::{Identifiable, Insertable, Queryable, Selectable};
 use time::PrimitiveDateTime;
 
@@ -32,3 +32,21 @@ pub struct ThemeNew {
     pub entity_type: EntityType,
     pub theme_name: String,
 }
+
+impl ThemeNew {
+    pub fn new(theme_id: String, theme_name: String, lineage: ThemeLineage) -> Self {
+        let now = date_time::now();
+
+        Self {
+            theme_id,
+            theme_name,
+            tenant_id: lineage.tenant_id().to_owned(),
+            org_id: lineage.org_id().cloned(),
+            merchant_id: lineage.merchant_id().cloned(),
+            profile_id: lineage.profile_id().cloned(),
+            entity_type: lineage.entity_type(),
+            created_at: now,
+            last_modified_at: now,
+        }
+    }
+}
diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs
index dbdba189ed1..3ab56266b55 100644
--- a/crates/router/src/configs/secrets_transformers.rs
+++ b/crates/router/src/configs/secrets_transformers.rs
@@ -545,5 +545,6 @@ pub(crate) async fn fetch_raw_secrets(
             .network_tokenization_supported_card_networks,
         network_tokenization_service,
         network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors,
+        theme_storage: conf.theme_storage,
     }
 }
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 4e559a261b9..1da4a33f5f3 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -128,6 +128,7 @@ pub struct Settings<S: SecretState> {
     pub network_tokenization_supported_card_networks: NetworkTokenizationSupportedCardNetworks,
     pub network_tokenization_service: Option<SecretStateContainer<NetworkTokenizationService, S>>,
     pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors,
+    pub theme_storage: FileStorageConfig,
 }
 
 #[derive(Debug, Deserialize, Clone, Default)]
@@ -886,6 +887,10 @@ impl Settings<SecuredSecret> {
             .validate()
             .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?;
 
+        self.theme_storage
+            .validate()
+            .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?;
+
         Ok(())
     }
 }
diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs
index 5c7b77246e2..33477df6330 100644
--- a/crates/router/src/core/errors/user.rs
+++ b/crates/router/src/core/errors/user.rs
@@ -96,6 +96,16 @@ pub enum UserErrors {
     MaxRecoveryCodeAttemptsReached,
     #[error("Forbidden tenant id")]
     ForbiddenTenantId,
+    #[error("Error Uploading file to Theme Storage")]
+    ErrorUploadingFile,
+    #[error("Error Retrieving file from Theme Storage")]
+    ErrorRetrievingFile,
+    #[error("Theme not found")]
+    ThemeNotFound,
+    #[error("Theme with lineage already exists")]
+    ThemeAlreadyExists,
+    #[error("Invalid field: {0} in lineage")]
+    InvalidThemeLineage(String),
 }
 
 impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
@@ -244,57 +254,93 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
             Self::ForbiddenTenantId => {
                 AER::BadRequest(ApiError::new(sub_code, 50, self.get_error_message(), None))
             }
+            Self::ErrorUploadingFile => AER::InternalServerError(ApiError::new(
+                sub_code,
+                51,
+                self.get_error_message(),
+                None,
+            )),
+            Self::ErrorRetrievingFile => AER::InternalServerError(ApiError::new(
+                sub_code,
+                52,
+                self.get_error_message(),
+                None,
+            )),
+            Self::ThemeNotFound => {
+                AER::NotFound(ApiError::new(sub_code, 53, self.get_error_message(), None))
+            }
+            Self::ThemeAlreadyExists => {
+                AER::BadRequest(ApiError::new(sub_code, 54, self.get_error_message(), None))
+            }
+            Self::InvalidThemeLineage(_) => {
+                AER::BadRequest(ApiError::new(sub_code, 55, self.get_error_message(), None))
+            }
         }
     }
 }
 
 impl UserErrors {
-    pub fn get_error_message(&self) -> &str {
+    pub fn get_error_message(&self) -> String {
         match self {
-            Self::InternalServerError => "Something went wrong",
-            Self::InvalidCredentials => "Incorrect email or password",
-            Self::UserNotFound => "Email doesn’t exist. Register",
-            Self::UserExists => "An account already exists with this email",
-            Self::LinkInvalid => "Invalid or expired link",
-            Self::UnverifiedUser => "Kindly verify your account",
-            Self::InvalidOldPassword => "Old password incorrect. Please enter the correct password",
-            Self::EmailParsingError => "Invalid Email",
-            Self::NameParsingError => "Invalid Name",
-            Self::PasswordParsingError => "Invalid Password",
-            Self::UserAlreadyVerified => "User already verified",
-            Self::CompanyNameParsingError => "Invalid Company Name",
-            Self::MerchantAccountCreationError(error_message) => error_message,
-            Self::InvalidEmailError => "Invalid Email",
-            Self::MerchantIdNotFound => "Invalid Merchant ID",
-            Self::MetadataAlreadySet => "Metadata already set",
-            Self::DuplicateOrganizationId => "An Organization with the id already exists",
-            Self::InvalidRoleId => "Invalid Role ID",
-            Self::InvalidRoleOperation => "User Role Operation Not Supported",
-            Self::IpAddressParsingFailed => "Something went wrong",
-            Self::InvalidMetadataRequest => "Invalid Metadata Request",
-            Self::MerchantIdParsingError => "Invalid Merchant Id",
-            Self::ChangePasswordError => "Old and new password cannot be same",
-            Self::InvalidDeleteOperation => "Delete Operation Not Supported",
-            Self::MaxInvitationsError => "Maximum invite count per request exceeded",
-            Self::RoleNotFound => "Role Not Found",
-            Self::InvalidRoleOperationWithMessage(error_message) => error_message,
-            Self::RoleNameParsingError => "Invalid Role Name",
-            Self::RoleNameAlreadyExists => "Role name already exists",
-            Self::TotpNotSetup => "TOTP not setup",
-            Self::InvalidTotp => "Invalid TOTP",
-            Self::TotpRequired => "TOTP required",
-            Self::InvalidRecoveryCode => "Invalid Recovery Code",
-            Self::MaxTotpAttemptsReached => "Maximum attempts reached for TOTP",
-            Self::MaxRecoveryCodeAttemptsReached => "Maximum attempts reached for Recovery Code",
-            Self::TwoFactorAuthRequired => "Two factor auth required",
-            Self::TwoFactorAuthNotSetup => "Two factor auth not setup",
-            Self::TotpSecretNotFound => "TOTP secret not found",
-            Self::UserAuthMethodAlreadyExists => "User auth method already exists",
-            Self::InvalidUserAuthMethodOperation => "Invalid user auth method operation",
-            Self::AuthConfigParsingError => "Auth config parsing error",
-            Self::SSOFailed => "Invalid SSO request",
-            Self::JwtProfileIdMissing => "profile_id missing in JWT",
-            Self::ForbiddenTenantId => "Forbidden tenant id",
+            Self::InternalServerError => "Something went wrong".to_string(),
+            Self::InvalidCredentials => "Incorrect email or password".to_string(),
+            Self::UserNotFound => "Email doesn’t exist. Register".to_string(),
+            Self::UserExists => "An account already exists with this email".to_string(),
+            Self::LinkInvalid => "Invalid or expired link".to_string(),
+            Self::UnverifiedUser => "Kindly verify your account".to_string(),
+            Self::InvalidOldPassword => {
+                "Old password incorrect. Please enter the correct password".to_string()
+            }
+            Self::EmailParsingError => "Invalid Email".to_string(),
+            Self::NameParsingError => "Invalid Name".to_string(),
+            Self::PasswordParsingError => "Invalid Password".to_string(),
+            Self::UserAlreadyVerified => "User already verified".to_string(),
+            Self::CompanyNameParsingError => "Invalid Company Name".to_string(),
+            Self::MerchantAccountCreationError(error_message) => error_message.to_string(),
+            Self::InvalidEmailError => "Invalid Email".to_string(),
+            Self::MerchantIdNotFound => "Invalid Merchant ID".to_string(),
+            Self::MetadataAlreadySet => "Metadata already set".to_string(),
+            Self::DuplicateOrganizationId => {
+                "An Organization with the id already exists".to_string()
+            }
+            Self::InvalidRoleId => "Invalid Role ID".to_string(),
+            Self::InvalidRoleOperation => "User Role Operation Not Supported".to_string(),
+            Self::IpAddressParsingFailed => "Something went wrong".to_string(),
+            Self::InvalidMetadataRequest => "Invalid Metadata Request".to_string(),
+            Self::MerchantIdParsingError => "Invalid Merchant Id".to_string(),
+            Self::ChangePasswordError => "Old and new password cannot be same".to_string(),
+            Self::InvalidDeleteOperation => "Delete Operation Not Supported".to_string(),
+            Self::MaxInvitationsError => "Maximum invite count per request exceeded".to_string(),
+            Self::RoleNotFound => "Role Not Found".to_string(),
+            Self::InvalidRoleOperationWithMessage(error_message) => error_message.to_string(),
+            Self::RoleNameParsingError => "Invalid Role Name".to_string(),
+            Self::RoleNameAlreadyExists => "Role name already exists".to_string(),
+            Self::TotpNotSetup => "TOTP not setup".to_string(),
+            Self::InvalidTotp => "Invalid TOTP".to_string(),
+            Self::TotpRequired => "TOTP required".to_string(),
+            Self::InvalidRecoveryCode => "Invalid Recovery Code".to_string(),
+            Self::MaxTotpAttemptsReached => "Maximum attempts reached for TOTP".to_string(),
+            Self::MaxRecoveryCodeAttemptsReached => {
+                "Maximum attempts reached for Recovery Code".to_string()
+            }
+            Self::TwoFactorAuthRequired => "Two factor auth required".to_string(),
+            Self::TwoFactorAuthNotSetup => "Two factor auth not setup".to_string(),
+            Self::TotpSecretNotFound => "TOTP secret not found".to_string(),
+            Self::UserAuthMethodAlreadyExists => "User auth method already exists".to_string(),
+            Self::InvalidUserAuthMethodOperation => {
+                "Invalid user auth method operation".to_string()
+            }
+            Self::AuthConfigParsingError => "Auth config parsing error".to_string(),
+            Self::SSOFailed => "Invalid SSO request".to_string(),
+            Self::JwtProfileIdMissing => "profile_id missing in JWT".to_string(),
+            Self::ForbiddenTenantId => "Forbidden tenant id".to_string(),
+            Self::ErrorUploadingFile => "Error Uploading file to Theme Storage".to_string(),
+            Self::ErrorRetrievingFile => "Error Retrieving file from Theme Storage".to_string(),
+            Self::ThemeNotFound => "Theme not found".to_string(),
+            Self::ThemeAlreadyExists => "Theme with lineage already exists".to_string(),
+            Self::InvalidThemeLineage(field_name) => {
+                format!("Invalid field: {} in lineage", field_name)
+            }
         }
     }
 }
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 039e891e422..027f89b5cc1 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -44,6 +44,7 @@ use crate::{
 pub mod dashboard_metadata;
 #[cfg(feature = "dummy_connector")]
 pub mod sample_data;
+pub mod theme;
 
 #[cfg(feature = "email")]
 pub async fn signup_with_merchant_id(
diff --git a/crates/router/src/core/user/theme.rs b/crates/router/src/core/user/theme.rs
new file mode 100644
index 00000000000..dbf11256a50
--- /dev/null
+++ b/crates/router/src/core/user/theme.rs
@@ -0,0 +1,225 @@
+use api_models::user::theme as theme_api;
+use common_utils::{
+    ext_traits::{ByteSliceExt, Encode},
+    types::theme::ThemeLineage,
+};
+use diesel_models::user::theme::ThemeNew;
+use error_stack::ResultExt;
+use hyperswitch_domain_models::api::ApplicationResponse;
+use masking::ExposeInterface;
+use rdkafka::message::ToBytes;
+use uuid::Uuid;
+
+use crate::{
+    core::errors::{StorageErrorExt, UserErrors, UserResponse},
+    routes::SessionState,
+    utils::user::theme as theme_utils,
+};
+
+pub async fn get_theme_using_lineage(
+    state: SessionState,
+    lineage: ThemeLineage,
+) -> UserResponse<theme_api::GetThemeResponse> {
+    let theme = state
+        .global_store
+        .find_theme_by_lineage(lineage)
+        .await
+        .to_not_found_response(UserErrors::ThemeNotFound)?;
+
+    let file = theme_utils::retrieve_file_from_theme_bucket(
+        &state,
+        &theme_utils::get_theme_file_key(&theme.theme_id),
+    )
+    .await?;
+
+    let parsed_data = file
+        .to_bytes()
+        .parse_struct("ThemeData")
+        .change_context(UserErrors::InternalServerError)?;
+
+    Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        theme_id: theme.theme_id,
+        theme_name: theme.theme_name,
+        entity_type: theme.entity_type,
+        tenant_id: theme.tenant_id,
+        org_id: theme.org_id,
+        merchant_id: theme.merchant_id,
+        profile_id: theme.profile_id,
+        theme_data: parsed_data,
+    }))
+}
+
+pub async fn get_theme_using_theme_id(
+    state: SessionState,
+    theme_id: String,
+) -> UserResponse<theme_api::GetThemeResponse> {
+    let theme = state
+        .global_store
+        .find_theme_by_theme_id(theme_id.clone())
+        .await
+        .to_not_found_response(UserErrors::ThemeNotFound)?;
+
+    let file = theme_utils::retrieve_file_from_theme_bucket(
+        &state,
+        &theme_utils::get_theme_file_key(&theme_id),
+    )
+    .await?;
+
+    let parsed_data = file
+        .to_bytes()
+        .parse_struct("ThemeData")
+        .change_context(UserErrors::InternalServerError)?;
+
+    Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        theme_id: theme.theme_id,
+        theme_name: theme.theme_name,
+        entity_type: theme.entity_type,
+        tenant_id: theme.tenant_id,
+        org_id: theme.org_id,
+        merchant_id: theme.merchant_id,
+        profile_id: theme.profile_id,
+        theme_data: parsed_data,
+    }))
+}
+
+pub async fn upload_file_to_theme_storage(
+    state: SessionState,
+    theme_id: String,
+    request: theme_api::UploadFileRequest,
+) -> UserResponse<()> {
+    let db_theme = state
+        .global_store
+        .find_theme_by_lineage(request.lineage)
+        .await
+        .to_not_found_response(UserErrors::ThemeNotFound)?;
+
+    if theme_id != db_theme.theme_id {
+        return Err(UserErrors::ThemeNotFound.into());
+    }
+
+    theme_utils::upload_file_to_theme_bucket(
+        &state,
+        &theme_utils::get_specific_file_key(&theme_id, &request.asset_name),
+        request.asset_data.expose(),
+    )
+    .await?;
+
+    Ok(ApplicationResponse::StatusOk)
+}
+
+pub async fn create_theme(
+    state: SessionState,
+    request: theme_api::CreateThemeRequest,
+) -> UserResponse<theme_api::GetThemeResponse> {
+    theme_utils::validate_lineage(&state, &request.lineage).await?;
+
+    let new_theme = ThemeNew::new(
+        Uuid::new_v4().to_string(),
+        request.theme_name,
+        request.lineage,
+    );
+
+    let db_theme = state
+        .global_store
+        .insert_theme(new_theme)
+        .await
+        .to_duplicate_response(UserErrors::ThemeAlreadyExists)?;
+
+    theme_utils::upload_file_to_theme_bucket(
+        &state,
+        &theme_utils::get_theme_file_key(&db_theme.theme_id),
+        request
+            .theme_data
+            .encode_to_vec()
+            .change_context(UserErrors::InternalServerError)?,
+    )
+    .await?;
+
+    let file = theme_utils::retrieve_file_from_theme_bucket(
+        &state,
+        &theme_utils::get_theme_file_key(&db_theme.theme_id),
+    )
+    .await?;
+
+    let parsed_data = file
+        .to_bytes()
+        .parse_struct("ThemeData")
+        .change_context(UserErrors::InternalServerError)?;
+
+    Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        theme_id: db_theme.theme_id,
+        entity_type: db_theme.entity_type,
+        tenant_id: db_theme.tenant_id,
+        org_id: db_theme.org_id,
+        merchant_id: db_theme.merchant_id,
+        profile_id: db_theme.profile_id,
+        theme_name: db_theme.theme_name,
+        theme_data: parsed_data,
+    }))
+}
+
+pub async fn update_theme(
+    state: SessionState,
+    theme_id: String,
+    request: theme_api::UpdateThemeRequest,
+) -> UserResponse<theme_api::GetThemeResponse> {
+    let db_theme = state
+        .global_store
+        .find_theme_by_lineage(request.lineage)
+        .await
+        .to_not_found_response(UserErrors::ThemeNotFound)?;
+
+    if theme_id != db_theme.theme_id {
+        return Err(UserErrors::ThemeNotFound.into());
+    }
+
+    theme_utils::upload_file_to_theme_bucket(
+        &state,
+        &theme_utils::get_theme_file_key(&db_theme.theme_id),
+        request
+            .theme_data
+            .encode_to_vec()
+            .change_context(UserErrors::InternalServerError)?,
+    )
+    .await?;
+
+    let file = theme_utils::retrieve_file_from_theme_bucket(
+        &state,
+        &theme_utils::get_theme_file_key(&db_theme.theme_id),
+    )
+    .await?;
+
+    let parsed_data = file
+        .to_bytes()
+        .parse_struct("ThemeData")
+        .change_context(UserErrors::InternalServerError)?;
+
+    Ok(ApplicationResponse::Json(theme_api::GetThemeResponse {
+        theme_id: db_theme.theme_id,
+        entity_type: db_theme.entity_type,
+        tenant_id: db_theme.tenant_id,
+        org_id: db_theme.org_id,
+        merchant_id: db_theme.merchant_id,
+        profile_id: db_theme.profile_id,
+        theme_name: db_theme.theme_name,
+        theme_data: parsed_data,
+    }))
+}
+
+pub async fn delete_theme(
+    state: SessionState,
+    theme_id: String,
+    lineage: ThemeLineage,
+) -> UserResponse<()> {
+    state
+        .global_store
+        .delete_theme_by_lineage_and_theme_id(theme_id.clone(), lineage)
+        .await
+        .to_not_found_response(UserErrors::ThemeNotFound)?;
+
+    // TODO (#6717): Delete theme folder from the theme storage.
+    // Currently there is no simple or easy way to delete a whole folder from S3.
+    // So, we are not deleting the theme folder from the theme storage.
+
+    Ok(ApplicationResponse::StatusOk)
+}
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index ec45d9e18d7..9a284f1399d 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -3723,6 +3723,13 @@ impl ThemeInterface for KafkaStore {
         self.diesel_store.insert_theme(theme).await
     }
 
+    async fn find_theme_by_theme_id(
+        &self,
+        theme_id: String,
+    ) -> CustomResult<storage::theme::Theme, errors::StorageError> {
+        self.diesel_store.find_theme_by_theme_id(theme_id).await
+    }
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
diff --git a/crates/router/src/db/user/theme.rs b/crates/router/src/db/user/theme.rs
index f1e4f4f794e..9b55a98d0ad 100644
--- a/crates/router/src/db/user/theme.rs
+++ b/crates/router/src/db/user/theme.rs
@@ -16,6 +16,11 @@ pub trait ThemeInterface {
         theme: storage::ThemeNew,
     ) -> CustomResult<storage::Theme, errors::StorageError>;
 
+    async fn find_theme_by_theme_id(
+        &self,
+        theme_id: String,
+    ) -> CustomResult<storage::Theme, errors::StorageError>;
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
@@ -41,6 +46,16 @@ impl ThemeInterface for Store {
             .map_err(|error| report!(errors::StorageError::from(error)))
     }
 
+    async fn find_theme_by_theme_id(
+        &self,
+        theme_id: String,
+    ) -> CustomResult<storage::Theme, errors::StorageError> {
+        let conn = connection::pg_connection_read(self).await?;
+        storage::Theme::find_by_theme_id(&conn, theme_id)
+            .await
+            .map_err(|error| report!(errors::StorageError::from(error)))
+    }
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
@@ -165,6 +180,24 @@ impl ThemeInterface for MockDb {
         Ok(theme)
     }
 
+    async fn find_theme_by_theme_id(
+        &self,
+        theme_id: String,
+    ) -> CustomResult<storage::Theme, errors::StorageError> {
+        let themes = self.themes.lock().await;
+        themes
+            .iter()
+            .find(|theme| theme.theme_id == theme_id)
+            .cloned()
+            .ok_or(
+                errors::StorageError::ValueNotFound(format!(
+                    "Theme with id {} not found",
+                    theme_id
+                ))
+                .into(),
+            )
+    }
+
     async fn find_theme_by_lineage(
         &self,
         lineage: ThemeLineage,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index bb8d0d4f2bd..9f4b413ac5b 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -110,6 +110,7 @@ pub struct SessionState {
     #[cfg(feature = "olap")]
     pub opensearch_client: Arc<OpenSearchClient>,
     pub grpc_client: Arc<GrpcClients>,
+    pub theme_storage_client: Arc<dyn FileStorageInterface>,
 }
 impl scheduler::SchedulerSessionState for SessionState {
     fn get_db(&self) -> Box<dyn SchedulerInterface> {
@@ -208,6 +209,7 @@ pub struct AppState {
     pub file_storage_client: Arc<dyn FileStorageInterface>,
     pub encryption_client: Arc<dyn EncryptionManagementInterface>,
     pub grpc_client: Arc<GrpcClients>,
+    pub theme_storage_client: Arc<dyn FileStorageInterface>,
 }
 impl scheduler::SchedulerAppState for AppState {
     fn get_tenants(&self) -> Vec<id_type::TenantId> {
@@ -367,6 +369,7 @@ impl AppState {
             let email_client = Arc::new(create_email_client(&conf).await);
 
             let file_storage_client = conf.file_storage.get_file_storage_client().await;
+            let theme_storage_client = conf.theme_storage.get_file_storage_client().await;
 
             let grpc_client = conf.grpc_client.get_grpc_client_interface().await;
 
@@ -387,6 +390,7 @@ impl AppState {
                 file_storage_client,
                 encryption_client,
                 grpc_client,
+                theme_storage_client,
             }
         })
         .await
@@ -472,6 +476,7 @@ impl AppState {
             #[cfg(feature = "olap")]
             opensearch_client: Arc::clone(&self.opensearch_client),
             grpc_client: Arc::clone(&self.grpc_client),
+            theme_storage_client: self.theme_storage_client.clone(),
         })
     }
 }
@@ -2127,6 +2132,23 @@ impl User {
                     .route(web::delete().to(user::delete_sample_data)),
             )
         }
+
+        route = route.service(
+            web::scope("/theme")
+                .service(
+                    web::resource("")
+                        .route(web::get().to(user::theme::get_theme_using_lineage))
+                        .route(web::post().to(user::theme::create_theme)),
+                )
+                .service(
+                    web::resource("/{theme_id}")
+                        .route(web::get().to(user::theme::get_theme_using_theme_id))
+                        .route(web::put().to(user::theme::update_theme))
+                        .route(web::post().to(user::theme::upload_file_to_theme_storage))
+                        .route(web::delete().to(user::theme::delete_theme)),
+                ),
+        );
+
         route
     }
 }
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 4d3718b967d..1c7db127ffc 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -259,7 +259,13 @@ impl From<Flow> for ApiIdentifier {
             | Flow::ListMerchantsForUserInOrg
             | Flow::ListProfileForUserInOrgAndMerchant
             | Flow::ListInvitationsForUser
-            | Flow::AuthSelect => Self::User,
+            | Flow::AuthSelect
+            | Flow::GetThemeUsingLineage
+            | Flow::GetThemeUsingThemeId
+            | Flow::UploadFileToThemeStorage
+            | Flow::CreateTheme
+            | Flow::UpdateTheme
+            | Flow::DeleteTheme => Self::User,
 
             Flow::ListRolesV2
             | Flow::ListInvitableRolesAtEntityLevel
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 8fc0dad452a..9f2060e43cb 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -1,3 +1,5 @@
+pub mod theme;
+
 use actix_web::{web, HttpRequest, HttpResponse};
 #[cfg(feature = "dummy_connector")]
 use api_models::user::sample_data::SampleDataRequest;
diff --git a/crates/router/src/routes/user/theme.rs b/crates/router/src/routes/user/theme.rs
new file mode 100644
index 00000000000..69a8e9a5378
--- /dev/null
+++ b/crates/router/src/routes/user/theme.rs
@@ -0,0 +1,145 @@
+use actix_multipart::form::MultipartForm;
+use actix_web::{web, HttpRequest, HttpResponse};
+use api_models::user::theme as theme_api;
+use common_utils::types::theme::ThemeLineage;
+use masking::Secret;
+use router_env::Flow;
+
+use crate::{
+    core::{api_locking, user::theme as theme_core},
+    routes::AppState,
+    services::{api, authentication as auth},
+};
+
+pub async fn get_theme_using_lineage(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    query: web::Query<ThemeLineage>,
+) -> HttpResponse {
+    let flow = Flow::GetThemeUsingLineage;
+    let lineage = query.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        lineage,
+        |state, _, lineage, _| theme_core::get_theme_using_lineage(state, lineage),
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn get_theme_using_theme_id(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+) -> HttpResponse {
+    let flow = Flow::GetThemeUsingThemeId;
+    let payload = path.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload,
+        |state, _, payload, _| theme_core::get_theme_using_theme_id(state, payload),
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn upload_file_to_theme_storage(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+    MultipartForm(payload): MultipartForm<theme_api::UploadFileAssetData>,
+    query: web::Query<ThemeLineage>,
+) -> HttpResponse {
+    let flow = Flow::UploadFileToThemeStorage;
+    let theme_id = path.into_inner();
+    let payload = theme_api::UploadFileRequest {
+        lineage: query.into_inner(),
+        asset_name: payload.asset_name.into_inner(),
+        asset_data: Secret::new(payload.asset_data.data.to_vec()),
+    };
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload,
+        |state, _, payload, _| {
+            theme_core::upload_file_to_theme_storage(state, theme_id.clone(), payload)
+        },
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn create_theme(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    payload: web::Json<theme_api::CreateThemeRequest>,
+) -> HttpResponse {
+    let flow = Flow::CreateTheme;
+    let payload = payload.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload,
+        |state, _, payload, _| theme_core::create_theme(state, payload),
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn update_theme(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+    payload: web::Json<theme_api::UpdateThemeRequest>,
+) -> HttpResponse {
+    let flow = Flow::UpdateTheme;
+    let theme_id = path.into_inner();
+    let payload = payload.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload,
+        |state, _, payload, _| theme_core::update_theme(state, theme_id.clone(), payload),
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn delete_theme(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+    query: web::Query<ThemeLineage>,
+) -> HttpResponse {
+    let flow = Flow::DeleteTheme;
+    let theme_id = path.into_inner();
+    let lineage = query.into_inner();
+
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        lineage,
+        |state, _, lineage, _| theme_core::delete_theme(state, theme_id.clone(), lineage),
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs
index 443db741ae3..281b95255c8 100644
--- a/crates/router/src/utils/user.rs
+++ b/crates/router/src/utils/user.rs
@@ -27,6 +27,7 @@ pub mod dashboard_metadata;
 pub mod password;
 #[cfg(feature = "dummy_connector")]
 pub mod sample_data;
+pub mod theme;
 pub mod two_factor_auth;
 
 impl UserFromToken {
diff --git a/crates/router/src/utils/user/theme.rs b/crates/router/src/utils/user/theme.rs
new file mode 100644
index 00000000000..13452380d9d
--- /dev/null
+++ b/crates/router/src/utils/user/theme.rs
@@ -0,0 +1,158 @@
+use std::path::PathBuf;
+
+use common_utils::{id_type, types::theme::ThemeLineage};
+use error_stack::ResultExt;
+use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore;
+
+use crate::{
+    core::errors::{StorageErrorExt, UserErrors, UserResult},
+    routes::SessionState,
+};
+
+fn get_theme_dir_key(theme_id: &str) -> PathBuf {
+    ["themes", theme_id].iter().collect()
+}
+
+pub fn get_specific_file_key(theme_id: &str, file_name: &str) -> PathBuf {
+    let mut path = get_theme_dir_key(theme_id);
+    path.push(file_name);
+    path
+}
+
+pub fn get_theme_file_key(theme_id: &str) -> PathBuf {
+    get_specific_file_key(theme_id, "theme.json")
+}
+
+fn path_buf_to_str(path: &PathBuf) -> UserResult<&str> {
+    path.to_str()
+        .ok_or(UserErrors::InternalServerError)
+        .attach_printable(format!("Failed to convert path {:#?} to string", path))
+}
+
+pub async fn retrieve_file_from_theme_bucket(
+    state: &SessionState,
+    path: &PathBuf,
+) -> UserResult<Vec<u8>> {
+    state
+        .theme_storage_client
+        .retrieve_file(path_buf_to_str(path)?)
+        .await
+        .change_context(UserErrors::ErrorRetrievingFile)
+}
+
+pub async fn upload_file_to_theme_bucket(
+    state: &SessionState,
+    path: &PathBuf,
+    data: Vec<u8>,
+) -> UserResult<()> {
+    state
+        .theme_storage_client
+        .upload_file(path_buf_to_str(path)?, data)
+        .await
+        .change_context(UserErrors::ErrorUploadingFile)
+}
+
+pub async fn validate_lineage(state: &SessionState, lineage: &ThemeLineage) -> UserResult<()> {
+    match lineage {
+        ThemeLineage::Organization { tenant_id, org_id } => {
+            validate_tenant(state, tenant_id)?;
+            validate_org(state, org_id).await?;
+            Ok(())
+        }
+        ThemeLineage::Merchant {
+            tenant_id,
+            org_id,
+            merchant_id,
+        } => {
+            validate_tenant(state, tenant_id)?;
+            validate_org(state, org_id).await?;
+            validate_merchant(state, org_id, merchant_id).await?;
+            Ok(())
+        }
+        ThemeLineage::Profile {
+            tenant_id,
+            org_id,
+            merchant_id,
+            profile_id,
+        } => {
+            validate_tenant(state, tenant_id)?;
+            validate_org(state, org_id).await?;
+            let key_store = validate_merchant_and_get_key_store(state, org_id, merchant_id).await?;
+            validate_profile(state, profile_id, merchant_id, &key_store).await?;
+            Ok(())
+        }
+    }
+}
+
+fn validate_tenant(state: &SessionState, tenant_id: &id_type::TenantId) -> UserResult<()> {
+    if &state.tenant.tenant_id != tenant_id {
+        return Err(UserErrors::InvalidThemeLineage("tenant_id".to_string()).into());
+    }
+    Ok(())
+}
+
+async fn validate_org(state: &SessionState, org_id: &id_type::OrganizationId) -> UserResult<()> {
+    state
+        .store
+        .find_organization_by_org_id(org_id)
+        .await
+        .to_not_found_response(UserErrors::InvalidThemeLineage("org_id".to_string()))?;
+    Ok(())
+}
+
+async fn validate_merchant_and_get_key_store(
+    state: &SessionState,
+    org_id: &id_type::OrganizationId,
+    merchant_id: &id_type::MerchantId,
+) -> UserResult<MerchantKeyStore> {
+    let key_store = state
+        .store
+        .get_merchant_key_store_by_merchant_id(
+            &state.into(),
+            merchant_id,
+            &state.store.get_master_key().to_vec().into(),
+        )
+        .await
+        .to_not_found_response(UserErrors::InvalidThemeLineage("merchant_id".to_string()))?;
+
+    let merchant_account = state
+        .store
+        .find_merchant_account_by_merchant_id(&state.into(), merchant_id, &key_store)
+        .await
+        .to_not_found_response(UserErrors::InvalidThemeLineage("merchant_id".to_string()))?;
+
+    if &merchant_account.organization_id != org_id {
+        return Err(UserErrors::InvalidThemeLineage("merchant_id".to_string()).into());
+    }
+
+    Ok(key_store)
+}
+
+async fn validate_merchant(
+    state: &SessionState,
+    org_id: &id_type::OrganizationId,
+    merchant_id: &id_type::MerchantId,
+) -> UserResult<()> {
+    validate_merchant_and_get_key_store(state, org_id, merchant_id)
+        .await
+        .map(|_| ())
+}
+
+async fn validate_profile(
+    state: &SessionState,
+    profile_id: &id_type::ProfileId,
+    merchant_id: &id_type::MerchantId,
+    key_store: &MerchantKeyStore,
+) -> UserResult<()> {
+    state
+        .store
+        .find_business_profile_by_merchant_id_profile_id(
+            &state.into(),
+            key_store,
+            merchant_id,
+            profile_id,
+        )
+        .await
+        .to_not_found_response(UserErrors::InvalidThemeLineage("profile_id".to_string()))?;
+    Ok(())
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index b1488b904be..0330c43aa45 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -483,6 +483,18 @@ pub enum Flow {
     ListUsersInLineage,
     /// List invitations for user
     ListInvitationsForUser,
+    /// Get theme using lineage
+    GetThemeUsingLineage,
+    /// Get theme using theme id
+    GetThemeUsingThemeId,
+    /// Upload file to theme storage
+    UploadFileToThemeStorage,
+    /// Create theme
+    CreateTheme,
+    /// Update theme
+    UpdateTheme,
+    /// Delete theme
+    DeleteTheme,
     /// List initial webhook delivery attempts
     WebhookEventInitialDeliveryAttemptList,
     /// List delivery attempts for a webhook event