Bug: Persist Last Used Lineage Context in Database Instead of Redis



Currently, the **last used lineage context** (org_id, merchant_id, profile_id, role_id, tenant_id) is cached temporarily in Redis with a 7-day TTL.
While this improves user experience across sessions, it introduces problems like:
- Redis is volatile — lineage context is lost if redis crashes
- Users switching between v1/v2 deployments have inconsistent Redis prefixes, leading to stale context (this has been fixed)
- Adds unnecessary dependency on Redis for critical authentication flow.

## Objectives:
- Move the lineage context storage from Redis to the database (preferably to the users table as a new column).
- Ensure lineage context is persistently stored and updated in DB.

## Tasks:
- [x] Add a new nullable last_used_lineage_context (JSON/JSONB) column to the users table.

- [x] Modify lineage context get and set helpers to use the DB instead of Redis.
diff --git a/crates/api_models/src/user/theme.rs b/crates/api_models/src/user/theme.rs
index 29b6a5d81b9..0616a5aa220 100644
--- a/crates/api_models/src/user/theme.rs
+++ b/crates/api_models/src/user/theme.rs
@@ -2,7 +2,7 @@ use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm};
 use common_enums::EntityType;
 use common_utils::{
     id_type,
-    types::theme::{EmailThemeConfig, ThemeLineage},
+    types::user::{EmailThemeConfig, ThemeLineage},
 };
 use masking::Secret;
 use serde::{Deserialize, Serialize};
diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs
index ab61cf6cca1..55dcc4e5bd2 100644
--- a/crates/common_utils/src/types.rs
+++ b/crates/common_utils/src/types.rs
@@ -3,8 +3,8 @@ pub mod keymanager;
 
 /// Enum for Authentication Level
 pub mod authentication;
-/// Enum for Theme Lineage
-pub mod theme;
+/// User related types
+pub mod user;
 
 /// types that are wrappers around primitive types
 pub mod primitive_wrappers;
diff --git a/crates/common_utils/src/types/user.rs b/crates/common_utils/src/types/user.rs
new file mode 100644
index 00000000000..e00f07309d1
--- /dev/null
+++ b/crates/common_utils/src/types/user.rs
@@ -0,0 +1,9 @@
+/// User related types
+pub mod core;
+
+/// Theme related types
+pub mod theme;
+
+pub use core::*;
+
+pub use theme::*;
diff --git a/crates/common_utils/src/types/user/core.rs b/crates/common_utils/src/types/user/core.rs
new file mode 100644
index 00000000000..6bdecb1283b
--- /dev/null
+++ b/crates/common_utils/src/types/user/core.rs
@@ -0,0 +1,28 @@
+use diesel::{deserialize::FromSqlRow, expression::AsExpression};
+
+use crate::id_type;
+
+/// Struct for lineageContext
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, AsExpression, FromSqlRow)]
+#[diesel(sql_type = diesel::sql_types::Jsonb)]
+pub struct LineageContext {
+    /// user_id: String
+    pub user_id: String,
+
+    /// merchant_id: MerchantId
+    pub merchant_id: id_type::MerchantId,
+
+    /// role_id: String
+    pub role_id: String,
+
+    /// org_id: OrganizationId
+    pub org_id: id_type::OrganizationId,
+
+    /// profile_id: ProfileId
+    pub profile_id: id_type::ProfileId,
+
+    /// tenant_id: TenantId
+    pub tenant_id: id_type::TenantId,
+}
+
+crate::impl_to_sql_from_sql_json!(LineageContext);
diff --git a/crates/common_utils/src/types/theme.rs b/crates/common_utils/src/types/user/theme.rs
similarity index 100%
rename from crates/common_utils/src/types/theme.rs
rename to crates/common_utils/src/types/user/theme.rs
diff --git a/crates/diesel_models/src/query/user/theme.rs b/crates/diesel_models/src/query/user/theme.rs
index 945ce1a9bb2..e8fcf135770 100644
--- a/crates/diesel_models/src/query/user/theme.rs
+++ b/crates/diesel_models/src/query/user/theme.rs
@@ -1,5 +1,5 @@
 use async_bb8_diesel::AsyncRunQueryDsl;
-use common_utils::types::theme::ThemeLineage;
+use common_utils::types::user::ThemeLineage;
 use diesel::{
     associations::HasTable,
     debug_query,
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index 46a36bab8c9..6f978a7afec 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1551,6 +1551,7 @@ diesel::table! {
         totp_secret -> Nullable<Bytea>,
         totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,
         last_password_modified_at -> Nullable<Timestamp>,
+        lineage_context -> Nullable<Jsonb>,
     }
 }
 
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index 5349ccf728c..caef78cf79a 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -1483,6 +1483,7 @@ diesel::table! {
         totp_secret -> Nullable<Bytea>,
         totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,
         last_password_modified_at -> Nullable<Timestamp>,
+        lineage_context -> Nullable<Jsonb>,
     }
 }
 
diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs
index cf584c09b12..a05e9768c09 100644
--- a/crates/diesel_models/src/user.rs
+++ b/crates/diesel_models/src/user.rs
@@ -1,4 +1,4 @@
-use common_utils::{encryption::Encryption, pii};
+use common_utils::{encryption::Encryption, pii, types::user::LineageContext};
 use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
 use masking::Secret;
 use time::PrimitiveDateTime;
@@ -24,6 +24,7 @@ pub struct User {
     #[diesel(deserialize_as = OptionalDieselArray<Secret<String>>)]
     pub totp_recovery_codes: Option<Vec<Secret<String>>>,
     pub last_password_modified_at: Option<PrimitiveDateTime>,
+    pub lineage_context: Option<LineageContext>,
 }
 
 #[derive(
@@ -42,6 +43,7 @@ pub struct UserNew {
     pub totp_secret: Option<Encryption>,
     pub totp_recovery_codes: Option<Vec<Secret<String>>>,
     pub last_password_modified_at: Option<PrimitiveDateTime>,
+    pub lineage_context: Option<LineageContext>,
 }
 
 #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
@@ -55,6 +57,7 @@ pub struct UserUpdateInternal {
     totp_secret: Option<Encryption>,
     totp_recovery_codes: Option<Vec<Secret<String>>>,
     last_password_modified_at: Option<PrimitiveDateTime>,
+    lineage_context: Option<LineageContext>,
 }
 
 #[derive(Debug)]
@@ -72,6 +75,9 @@ pub enum UserUpdate {
     PasswordUpdate {
         password: Secret<String>,
     },
+    LineageContextUpdate {
+        lineage_context: LineageContext,
+    },
 }
 
 impl From<UserUpdate> for UserUpdateInternal {
@@ -87,6 +93,7 @@ impl From<UserUpdate> for UserUpdateInternal {
                 totp_secret: None,
                 totp_recovery_codes: None,
                 last_password_modified_at: None,
+                lineage_context: None,
             },
             UserUpdate::AccountUpdate { name, is_verified } => Self {
                 name,
@@ -97,6 +104,7 @@ impl From<UserUpdate> for UserUpdateInternal {
                 totp_secret: None,
                 totp_recovery_codes: None,
                 last_password_modified_at: None,
+                lineage_context: None,
             },
             UserUpdate::TotpUpdate {
                 totp_status,
@@ -111,6 +119,7 @@ impl From<UserUpdate> for UserUpdateInternal {
                 totp_secret,
                 totp_recovery_codes,
                 last_password_modified_at: None,
+                lineage_context: None,
             },
             UserUpdate::PasswordUpdate { password } => Self {
                 name: None,
@@ -121,6 +130,18 @@ impl From<UserUpdate> for UserUpdateInternal {
                 totp_status: None,
                 totp_secret: None,
                 totp_recovery_codes: None,
+                lineage_context: None,
+            },
+            UserUpdate::LineageContextUpdate { lineage_context } => Self {
+                name: None,
+                password: None,
+                is_verified: None,
+                last_modified_at,
+                last_password_modified_at: None,
+                totp_status: None,
+                totp_secret: None,
+                totp_recovery_codes: None,
+                lineage_context: Some(lineage_context),
             },
         }
     }
diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs
index 46cc90a45ec..fad45995649 100644
--- a/crates/diesel_models/src/user/theme.rs
+++ b/crates/diesel_models/src/user/theme.rs
@@ -1,7 +1,7 @@
 use common_enums::EntityType;
 use common_utils::{
     date_time, id_type,
-    types::theme::{EmailThemeConfig, ThemeLineage},
+    types::user::{EmailThemeConfig, ThemeLineage},
 };
 use diesel::{Identifiable, Insertable, Queryable, Selectable};
 use time::PrimitiveDateTime;
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 10a72c4e82d..b0fd269bf79 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -7,7 +7,7 @@ use std::{
 #[cfg(feature = "olap")]
 use analytics::{opensearch::OpenSearchConfig, ReportConfig};
 use api_models::enums;
-use common_utils::{ext_traits::ConfigExt, id_type, types::theme::EmailThemeConfig};
+use common_utils::{ext_traits::ConfigExt, id_type, types::user::EmailThemeConfig};
 use config::{Environment, File};
 use error_stack::ResultExt;
 #[cfg(feature = "email")]
diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs
index 649b8820859..4f681cda25b 100644
--- a/crates/router/src/consts/user.rs
+++ b/crates/router/src/consts/user.rs
@@ -41,6 +41,3 @@ pub const REDIS_SSO_TTL: i64 = 5 * 60; // 5 minutes
 pub const DEFAULT_PROFILE_NAME: &str = "default";
 pub const DEFAULT_PRODUCT_TYPE: common_enums::MerchantProductType =
     common_enums::MerchantProductType::Orchestration;
-
-pub const LINEAGE_CONTEXT_TIME_EXPIRY_IN_SECS: i64 = 60 * 60 * 24 * 7; // 7 days
-pub const LINEAGE_CONTEXT_PREFIX: &str = "LINEAGE_CONTEXT_";
diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs
index 3b2aacd4514..17e86dc41ca 100644
--- a/crates/router/src/core/recon.rs
+++ b/crates/router/src/core/recon.rs
@@ -1,6 +1,6 @@
 use api_models::recon as recon_api;
 #[cfg(feature = "email")]
-use common_utils::{ext_traits::AsyncExt, types::theme::ThemeLineage};
+use common_utils::{ext_traits::AsyncExt, types::user::ThemeLineage};
 use error_stack::ResultExt;
 #[cfg(feature = "email")]
 use masking::{ExposeInterface, PeekInterface, Secret};
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index ff90ca1dfac..9802edfd346 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -8,7 +8,10 @@ use api_models::{
     user::{self as user_api, InviteMultipleUserResponse, NameIdUnit},
 };
 use common_enums::{EntityType, UserAuthType};
-use common_utils::{type_name, types::keymanager::Identifier};
+use common_utils::{
+    type_name,
+    types::{keymanager::Identifier, user::LineageContext},
+};
 #[cfg(feature = "email")]
 use diesel_models::user_role::UserRoleUpdate;
 use diesel_models::{
@@ -3237,7 +3240,7 @@ pub async fn switch_org_for_user(
         }
     };
 
-    let lineage_context = domain::LineageContext {
+    let lineage_context = LineageContext {
         user_id: user_from_token.user_id.clone(),
         merchant_id: merchant_id.clone(),
         role_id: role_id.clone(),
@@ -3250,9 +3253,11 @@ pub async fn switch_org_for_user(
             .clone(),
     };
 
-    lineage_context
-        .try_set_lineage_context_in_cache(&state, user_from_token.user_id.as_str())
-        .await;
+    utils::user::spawn_async_lineage_context_update_to_db(
+        &state,
+        &user_from_token.user_id,
+        lineage_context,
+    );
 
     let token = utils::user::generate_jwt_auth_token_with_attributes(
         &state,
@@ -3449,7 +3454,7 @@ pub async fn switch_merchant_for_user_in_org(
         }
     };
 
-    let lineage_context = domain::LineageContext {
+    let lineage_context = LineageContext {
         user_id: user_from_token.user_id.clone(),
         merchant_id: merchant_id.clone(),
         role_id: role_id.clone(),
@@ -3462,9 +3467,11 @@ pub async fn switch_merchant_for_user_in_org(
             .clone(),
     };
 
-    lineage_context
-        .try_set_lineage_context_in_cache(&state, user_from_token.user_id.as_str())
-        .await;
+    utils::user::spawn_async_lineage_context_update_to_db(
+        &state,
+        &user_from_token.user_id,
+        lineage_context,
+    );
 
     let token = utils::user::generate_jwt_auth_token_with_attributes(
         &state,
@@ -3582,7 +3589,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant(
         }
     };
 
-    let lineage_context = domain::LineageContext {
+    let lineage_context = LineageContext {
         user_id: user_from_token.user_id.clone(),
         merchant_id: user_from_token.merchant_id.clone(),
         role_id: role_id.clone(),
@@ -3595,9 +3602,11 @@ pub async fn switch_profile_for_user_in_org_and_merchant(
             .clone(),
     };
 
-    lineage_context
-        .try_set_lineage_context_in_cache(&state, user_from_token.user_id.as_str())
-        .await;
+    utils::user::spawn_async_lineage_context_update_to_db(
+        &state,
+        &user_from_token.user_id,
+        lineage_context,
+    );
 
     let token = utils::user::generate_jwt_auth_token_with_attributes(
         &state,
diff --git a/crates/router/src/core/user/theme.rs b/crates/router/src/core/user/theme.rs
index 2a896b7158f..d5c9e653066 100644
--- a/crates/router/src/core/user/theme.rs
+++ b/crates/router/src/core/user/theme.rs
@@ -1,7 +1,7 @@
 use api_models::user::theme as theme_api;
 use common_utils::{
     ext_traits::{ByteSliceExt, Encode},
-    types::theme::ThemeLineage,
+    types::user::ThemeLineage,
 };
 use diesel_models::user::theme::ThemeNew;
 use error_stack::ResultExt;
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index be903ef3bcc..1ef8178580b 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -5,7 +5,7 @@ use common_enums::enums::MerchantStorageScheme;
 use common_utils::{
     errors::CustomResult,
     id_type,
-    types::{keymanager::KeyManagerState, theme::ThemeLineage},
+    types::{keymanager::KeyManagerState, user::ThemeLineage},
 };
 #[cfg(feature = "v2")]
 use diesel_models::ephemeral_key::{ClientSecretType, ClientSecretTypeNew};
diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs
index 089c9fc6eb7..008c1f721d0 100644
--- a/crates/router/src/db/user.rs
+++ b/crates/router/src/db/user.rs
@@ -163,6 +163,7 @@ impl UserInterface for MockDb {
             totp_secret: user_data.totp_secret,
             totp_recovery_codes: user_data.totp_recovery_codes,
             last_password_modified_at: user_data.last_password_modified_at,
+            lineage_context: user_data.lineage_context,
         };
         users.push(user.clone());
         Ok(user)
@@ -208,17 +209,20 @@ impl UserInterface for MockDb {
         update_user: storage::UserUpdate,
     ) -> CustomResult<storage::User, errors::StorageError> {
         let mut users = self.users.lock().await;
+        let last_modified_at = common_utils::date_time::now();
         users
             .iter_mut()
             .find(|user| user.user_id == user_id)
             .map(|user| {
                 *user = match &update_user {
                     storage::UserUpdate::VerifyUser => storage::User {
+                        last_modified_at,
                         is_verified: true,
                         ..user.to_owned()
                     },
                     storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User {
                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
+                        last_modified_at,
                         is_verified: is_verified.unwrap_or(user.is_verified),
                         ..user.to_owned()
                     },
@@ -227,6 +231,7 @@ impl UserInterface for MockDb {
                         totp_secret,
                         totp_recovery_codes,
                     } => storage::User {
+                        last_modified_at,
                         totp_status: totp_status.unwrap_or(user.totp_status),
                         totp_secret: totp_secret.clone().or(user.totp_secret.clone()),
                         totp_recovery_codes: totp_recovery_codes
@@ -239,6 +244,13 @@ impl UserInterface for MockDb {
                         last_password_modified_at: Some(common_utils::date_time::now()),
                         ..user.to_owned()
                     },
+                    storage::UserUpdate::LineageContextUpdate { lineage_context } => {
+                        storage::User {
+                            last_modified_at,
+                            lineage_context: Some(lineage_context.clone()),
+                            ..user.to_owned()
+                        }
+                    }
                 };
                 user.to_owned()
             })
@@ -256,17 +268,20 @@ impl UserInterface for MockDb {
         update_user: storage::UserUpdate,
     ) -> CustomResult<storage::User, errors::StorageError> {
         let mut users = self.users.lock().await;
+        let last_modified_at = common_utils::date_time::now();
         users
             .iter_mut()
             .find(|user| user.email.eq(user_email.get_inner()))
             .map(|user| {
                 *user = match &update_user {
                     storage::UserUpdate::VerifyUser => storage::User {
+                        last_modified_at,
                         is_verified: true,
                         ..user.to_owned()
                     },
                     storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User {
                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
+                        last_modified_at,
                         is_verified: is_verified.unwrap_or(user.is_verified),
                         ..user.to_owned()
                     },
@@ -275,6 +290,7 @@ impl UserInterface for MockDb {
                         totp_secret,
                         totp_recovery_codes,
                     } => storage::User {
+                        last_modified_at,
                         totp_status: totp_status.unwrap_or(user.totp_status),
                         totp_secret: totp_secret.clone().or(user.totp_secret.clone()),
                         totp_recovery_codes: totp_recovery_codes
@@ -287,6 +303,13 @@ impl UserInterface for MockDb {
                         last_password_modified_at: Some(common_utils::date_time::now()),
                         ..user.to_owned()
                     },
+                    storage::UserUpdate::LineageContextUpdate { lineage_context } => {
+                        storage::User {
+                            last_modified_at,
+                            lineage_context: Some(lineage_context.clone()),
+                            ..user.to_owned()
+                        }
+                    }
                 };
                 user.to_owned()
             })
diff --git a/crates/router/src/db/user/theme.rs b/crates/router/src/db/user/theme.rs
index 51c0a52b3a0..4202b69d99d 100644
--- a/crates/router/src/db/user/theme.rs
+++ b/crates/router/src/db/user/theme.rs
@@ -1,4 +1,4 @@
-use common_utils::types::theme::ThemeLineage;
+use common_utils::types::user::ThemeLineage;
 use diesel_models::user::theme as storage;
 use error_stack::report;
 
diff --git a/crates/router/src/routes/user/theme.rs b/crates/router/src/routes/user/theme.rs
index 69a8e9a5378..6f6926dadc3 100644
--- a/crates/router/src/routes/user/theme.rs
+++ b/crates/router/src/routes/user/theme.rs
@@ -1,7 +1,7 @@
 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 common_utils::types::user::ThemeLineage;
 use masking::Secret;
 use router_env::Flow;
 
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index d663f72cec7..0328ea988d4 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -1,6 +1,6 @@
 use api_models::user::dashboard_metadata::ProdIntent;
 use common_enums::{EntityType, MerchantProductType};
-use common_utils::{errors::CustomResult, pii, types::theme::EmailThemeConfig};
+use common_utils::{errors::CustomResult, pii, types::user::EmailThemeConfig};
 use error_stack::ResultExt;
 use external_services::email::{EmailContents, EmailData, EmailError};
 use masking::{ExposeInterface, Secret};
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index ca48d424d2a..45eb094053a 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -23,7 +23,6 @@ use hyperswitch_domain_models::api::ApplicationResponse;
 use masking::{ExposeInterface, PeekInterface, Secret};
 use once_cell::sync::Lazy;
 use rand::distributions::{Alphanumeric, DistString};
-use router_env::logger;
 use time::PrimitiveDateTime;
 use unicode_segmentation::UnicodeSegmentation;
 #[cfg(feature = "keymanager_create")]
@@ -925,6 +924,7 @@ impl TryFrom<NewUser> for storage_user::UserNew {
             last_password_modified_at: value
                 .password
                 .and_then(|password_inner| password_inner.is_temporary.not().then_some(now)),
+            lineage_context: None,
         })
     }
 }
@@ -1499,54 +1499,3 @@ where
             .change_context(UserErrors::InternalServerError)
     }
 }
-
-#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
-pub struct LineageContext {
-    pub user_id: String,
-    pub merchant_id: id_type::MerchantId,
-    pub role_id: String,
-    pub org_id: id_type::OrganizationId,
-    pub profile_id: id_type::ProfileId,
-    pub tenant_id: id_type::TenantId,
-}
-
-impl LineageContext {
-    pub async fn try_get_lineage_context_from_cache(
-        state: &SessionState,
-        user_id: &str,
-    ) -> Option<Self> {
-        // The errors are not handled here because we don't want to fail the request if the cache operation fails.
-        // The errors are logged for debugging purposes.
-        match utils::user::get_lineage_context_from_cache(state, user_id).await {
-            Ok(Some(ctx)) => Some(ctx),
-            Ok(None) => {
-                logger::debug!("Lineage context not found in Redis for user {}", user_id);
-                None
-            }
-            Err(e) => {
-                logger::error!(
-                    "Failed to retrieve lineage context from Redis for user {}: {:?}",
-                    user_id,
-                    e
-                );
-                None
-            }
-        }
-    }
-
-    pub async fn try_set_lineage_context_in_cache(&self, state: &SessionState, user_id: &str) {
-        // The errors are not handled here because we don't want to fail the request if the cache operation fails.
-        // The errors are logged for debugging purposes.
-        if let Err(e) =
-            utils::user::set_lineage_context_in_cache(state, user_id, self.clone()).await
-        {
-            logger::error!(
-                "Failed to set lineage context in Redis for user {}: {:?}",
-                user_id,
-                e
-            );
-        } else {
-            logger::debug!("Lineage context cached for user {}", user_id);
-        }
-    }
-}
diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs
index 0214d3cdd15..7bc11383da0 100644
--- a/crates/router/src/types/domain/user/decision_manager.rs
+++ b/crates/router/src/types/domain/user/decision_manager.rs
@@ -1,11 +1,12 @@
 use common_enums::TokenPurpose;
-use common_utils::id_type;
+use common_utils::{id_type, types::user::LineageContext};
 use diesel_models::{
     enums::{UserRoleVersion, UserStatus},
     user_role::UserRole,
 };
 use error_stack::ResultExt;
 use masking::Secret;
+use router_env::logger;
 
 use super::UserFromStorage;
 use crate::{
@@ -13,7 +14,6 @@ use crate::{
     db::user_role::ListUserRolesByUserIdPayload,
     routes::SessionState,
     services::authentication as auth,
-    types::domain::LineageContext,
     utils,
 };
 
@@ -129,13 +129,25 @@ impl JWTFlow {
         user_role: &UserRole,
     ) -> UserResult<Secret<String>> {
         let user_id = next_flow.user.get_user_id();
-        let cached_lineage_context =
-            LineageContext::try_get_lineage_context_from_cache(state, user_id).await;
+        // Fetch lineage context from DB
+        let lineage_context_from_db = state
+            .global_store
+            .find_user_by_id(user_id)
+            .await
+            .inspect_err(|e| {
+                logger::error!(
+                    "Failed to fetch lineage context from DB for user {}: {:?}",
+                    user_id,
+                    e
+                )
+            })
+            .ok()
+            .and_then(|user| user.lineage_context);
 
-        let new_lineage_context = match cached_lineage_context {
+        let new_lineage_context = match lineage_context_from_db {
             Some(ctx) => {
                 let tenant_id = ctx.tenant_id.clone();
-                let user_role_match_v1 = state
+                let user_role_match_v2 = state
                     .global_store
                     .find_user_role_by_user_id_and_lineage(
                         &ctx.user_id,
@@ -143,15 +155,15 @@ impl JWTFlow {
                         &ctx.org_id,
                         &ctx.merchant_id,
                         &ctx.profile_id,
-                        UserRoleVersion::V1,
+                        UserRoleVersion::V2,
                     )
                     .await
                     .is_ok();
 
-                if user_role_match_v1 {
+                if user_role_match_v2 {
                     ctx
                 } else {
-                    let user_role_match_v2 = state
+                    let user_role_match_v1 = state
                         .global_store
                         .find_user_role_by_user_id_and_lineage(
                             &ctx.user_id,
@@ -159,12 +171,12 @@ impl JWTFlow {
                             &ctx.org_id,
                             &ctx.merchant_id,
                             &ctx.profile_id,
-                            UserRoleVersion::V2,
+                            UserRoleVersion::V1,
                         )
                         .await
                         .is_ok();
 
-                    if user_role_match_v2 {
+                    if user_role_match_v1 {
                         ctx
                     } else {
                         // fallback to default lineage if cached context is invalid
@@ -179,9 +191,11 @@ impl JWTFlow {
             }
         };
 
-        new_lineage_context
-            .try_set_lineage_context_in_cache(state, user_id)
-            .await;
+        utils::user::spawn_async_lineage_context_update_to_db(
+            state,
+            user_id,
+            new_lineage_context.clone(),
+        );
 
         auth::AuthToken::new_token(
             new_lineage_context.user_id,
diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs
index 0c5fd187f92..ea3edd8dfc7 100644
--- a/crates/router/src/utils/user.rs
+++ b/crates/router/src/utils/user.rs
@@ -3,19 +3,19 @@ use std::sync::Arc;
 use api_models::user as user_api;
 use common_enums::UserAuthType;
 use common_utils::{
-    encryption::Encryption, errors::CustomResult, id_type, type_name, types::keymanager::Identifier,
+    encryption::Encryption,
+    errors::CustomResult,
+    id_type, type_name,
+    types::{keymanager::Identifier, user::LineageContext},
 };
 use diesel_models::organization::{self, OrganizationBridge};
 use error_stack::ResultExt;
 use masking::{ExposeInterface, Secret};
 use redis_interface::RedisConnectionPool;
-use router_env::env;
+use router_env::{env, logger};
 
 use crate::{
-    consts::user::{
-        LINEAGE_CONTEXT_PREFIX, LINEAGE_CONTEXT_TIME_EXPIRY_IN_SECS, REDIS_SSO_PREFIX,
-        REDIS_SSO_TTL,
-    },
+    consts::user::{REDIS_SSO_PREFIX, REDIS_SSO_TTL},
     core::errors::{StorageError, UserErrors, UserResult},
     routes::SessionState,
     services::{
@@ -23,7 +23,7 @@ use crate::{
         authorization::roles::RoleInfo,
     },
     types::{
-        domain::{self, LineageContext, MerchantAccount, UserFromStorage},
+        domain::{self, MerchantAccount, UserFromStorage},
         transformers::ForeignFrom,
     },
 };
@@ -341,50 +341,35 @@ pub async fn validate_email_domain_auth_type_using_db(
     .ok_or(UserErrors::InvalidUserAuthMethodOperation.into())
 }
 
-pub async fn get_lineage_context_from_cache(
-    state: &SessionState,
-    user_id: &str,
-) -> UserResult<Option<LineageContext>> {
-    let connection = get_redis_connection_for_global_tenant(state)?;
-    let key = format!("{}{}", LINEAGE_CONTEXT_PREFIX, user_id);
-    let lineage_context = connection
-        .get_key::<Option<String>>(&key.into())
-        .await
-        .change_context(UserErrors::InternalServerError)
-        .attach_printable("Failed to get lineage context from redis")?;
-
-    match lineage_context {
-        Some(json_str) => {
-            let ctx = serde_json::from_str::<LineageContext>(&json_str)
-                .change_context(UserErrors::InternalServerError)
-                .attach_printable("Failed to deserialize LineageContext from JSON")?;
-            Ok(Some(ctx))
-        }
-        None => Ok(None),
-    }
-}
-
-pub async fn set_lineage_context_in_cache(
+pub fn spawn_async_lineage_context_update_to_db(
     state: &SessionState,
     user_id: &str,
     lineage_context: LineageContext,
-) -> UserResult<()> {
-    let connection = get_redis_connection_for_global_tenant(state)?;
-    let key = format!("{}{}", LINEAGE_CONTEXT_PREFIX, user_id);
-    let serialized_lineage_context: String = serde_json::to_string(&lineage_context)
-        .change_context(UserErrors::InternalServerError)
-        .attach_printable("Failed to serialize LineageContext")?;
-    connection
-        .set_key_with_expiry(
-            &key.into(),
-            serialized_lineage_context,
-            LINEAGE_CONTEXT_TIME_EXPIRY_IN_SECS,
-        )
-        .await
-        .change_context(UserErrors::InternalServerError)
-        .attach_printable("Failed to set lineage context in redis")?;
-
-    Ok(())
+) {
+    let state = state.clone();
+    let lineage_context = lineage_context.clone();
+    let user_id = user_id.to_owned();
+    tokio::spawn(async move {
+        match state
+            .global_store
+            .update_user_by_user_id(
+                &user_id,
+                diesel_models::user::UserUpdate::LineageContextUpdate { lineage_context },
+            )
+            .await
+        {
+            Ok(_) => {
+                logger::debug!("Successfully updated lineage context for user {}", user_id);
+            }
+            Err(e) => {
+                logger::error!(
+                    "Failed to update lineage context for user {}: {:?}",
+                    user_id,
+                    e
+                );
+            }
+        }
+    });
 }
 
 pub fn generate_env_specific_merchant_id(value: String) -> UserResult<id_type::MerchantId> {
diff --git a/crates/router/src/utils/user/theme.rs b/crates/router/src/utils/user/theme.rs
index 0b9d4e2829a..33e1450f097 100644
--- a/crates/router/src/utils/user/theme.rs
+++ b/crates/router/src/utils/user/theme.rs
@@ -1,7 +1,7 @@
 use std::path::PathBuf;
 
 use common_enums::EntityType;
-use common_utils::{ext_traits::AsyncExt, id_type, types::theme::ThemeLineage};
+use common_utils::{ext_traits::AsyncExt, id_type, types::user::ThemeLineage};
 use diesel_models::user::theme::Theme;
 use error_stack::ResultExt;
 use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore;
diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs
index 2ff527a046e..4df78847b39 100644
--- a/crates/router/src/workflows/api_key_expiry.rs
+++ b/crates/router/src/workflows/api_key_expiry.rs
@@ -1,4 +1,4 @@
-use common_utils::{errors::ValidationError, ext_traits::ValueExt, types::theme::ThemeLineage};
+use common_utils::{errors::ValidationError, ext_traits::ValueExt, types::user::ThemeLineage};
 use diesel_models::{
     enums as storage_enums, process_tracker::business_status, ApiKeyExpiryTrackingData,
 };
diff --git a/migrations/2025-04-29-144409_add_lineage_context_to_users/down.sql b/migrations/2025-04-29-144409_add_lineage_context_to_users/down.sql
new file mode 100644
index 00000000000..989e7ae64a0
--- /dev/null
+++ b/migrations/2025-04-29-144409_add_lineage_context_to_users/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE users DROP COLUMN IF EXISTS lineage_context;
diff --git a/migrations/2025-04-29-144409_add_lineage_context_to_users/up.sql b/migrations/2025-04-29-144409_add_lineage_context_to_users/up.sql
new file mode 100644
index 00000000000..cbae98f97cc
--- /dev/null
+++ b/migrations/2025-04-29-144409_add_lineage_context_to_users/up.sql
@@ -0,0 +1,2 @@
+-- Your SQL goes here
+ALTER TABLE users ADD COLUMN IF NOT EXISTS lineage_context JSONB;

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [x] Enhancement
- [x] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Previously, the user's `lineage_context` (userid, org_id, merchant_id, profile_id, role_id, tenant_id) was cached in Redis.
This PR moves the storage and retrieval of `lineage_context` to the PostgreSQL `users` table instead of Redis.

This ensures permanent persistence of lineage_context in DB, as opposed to the 7 days TTL when stored in Redis.

### Key Changes

- **Schema Change:**
  - Added `lineage_context` as a `JSONB` column in the `users` table.

- **During Login:**
  - Attempt to fetch `lineage_context` from DB (`users.lineage_context`).
  - If present:
    - Validate the context by checking if a matching user role exists (checks both v1 and v2 user roles).
    - If valid → use it for JWT generation.
    - If not → fallback to default DB-based role resolution.
  - If not present → fallback directly to default role resolution.
  - DB update for the new lineage context is now done via `tokio::spawn` to avoid blocking response.


- **During Org/Merchant/Profile Switch:**
  - After switching, the new `lineage_context` is serialized and persisted in the `users` table.
  - This enables restoring the last used context during the next login.
  - The update is done in the background using `tokio::spawn` with success and failure logs.


- **Error Handling:**
  - All DB failures or deserialization errors are logged.
  - Failures do not interrupt the login/switch flow — safe fallback ensures continuity.

### 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
<!--
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).
-->
By moving the `lineage_context` to the `users` table in DB:

- We ensure the context is consistently available regardless of deployment environment.
- The last used account context becomes part of the user's persistent record.
- It simplifies logic and avoids prefix mismatches across tenants.

## 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)?
-->

<img width="1732" alt="Screenshot 2025-04-29 at 11 45 43 PM" src="https://github.com/user-attachments/assets/77a3ab50-add4-4091-b291-0bbe178bf617" />

![image](https://github.com/user-attachments/assets/1fc5ec61-3dc4-43e9-9fe1-af66fa7127c4)


#### Test Case 1: Lineage Context Caching on Login
1. Log in as a user with an assigned role.
2. Check that `lineage_context` is stored in the `users` table (PostgreSQL).
3. Log out and log back in → verify that the JWT reflects the same org/merchant/profile context without role fallback.

#### Test Case 2: Lineage Context Update on Org/Profile Switch
1. Log in as a user and switch to another org/merchant/profile.
2. Confirm that the updated context is persisted in the `lineage_context` field in the DB.
3. Log out and log in again → JWT should reflect the newly switched context.

#### Test Case 3: Fallback to Role Resolution upon clearing lineage_context in db (Tested on local)
1. Manually removed `lineage_context` from the DB for a user.
2. Logged in again → verified that the system correctly falls back to the default role resolution logic.
3. Confirm that the context is repopulated in the DB after login.

#### Test Case 4: Fallback to Role Resolution upon deleting user_role (Tested on local)
1. Manually deleted `user_role` from the DB for a user (user_role corresponds to user invited to another account)
2. Tried to log in again → verified that the system correctly falls back to the default role resolution logic.
3. Confirm that the context is repopulated in the DB after login.


## 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
