diff --git a/go.mod b/go.mod
index 782a306f4..b002ac377 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/gorilla/websocket v1.5.1
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/openimsdk/protocol v0.0.72-alpha.81
+	github.com/openimsdk/protocol v0.0.73-alpha.3
 	github.com/openimsdk/tools v0.0.50-alpha.79
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_golang v1.18.0
@@ -219,3 +219,5 @@ require (
 	golang.org/x/crypto v0.27.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 )
+
+//replace github.com/openimsdk/protocol => /Users/chao/Desktop/code/protocol
diff --git a/go.sum b/go.sum
index aa0dfa6ac..e6408cfcd 100644
--- a/go.sum
+++ b/go.sum
@@ -347,8 +347,8 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
 github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
 github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrkXcDACCqtyM=
 github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI=
-github.com/openimsdk/protocol v0.0.72-alpha.81 h1:6tDuZ3Anfi1uhX/V5mWxITqJnGQPnvgeaxeqJlEHIVE=
-github.com/openimsdk/protocol v0.0.72-alpha.81/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw=
+github.com/openimsdk/protocol v0.0.73-alpha.3 h1:mf/REUZA5in2gk8ggwqJD8444xLvB7WlF7M97oXN78g=
+github.com/openimsdk/protocol v0.0.73-alpha.3/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw=
 github.com/openimsdk/tools v0.0.50-alpha.79 h1:jxYEbrzaze4Z2r4NrKad816buZ690ix0L9MTOOOH3ik=
 github.com/openimsdk/tools v0.0.50-alpha.79/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo=
 github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
diff --git a/internal/api/router.go b/internal/api/router.go
index 657493b23..920bd5366 100644
--- a/internal/api/router.go
+++ b/internal/api/router.go
@@ -125,6 +125,11 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin
 		userRouterGroup.POST("/add_notification_account", u.AddNotificationAccount)
 		userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo)
 		userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount)
+
+		userRouterGroup.POST("/get_user_client_config", u.GetUserClientConfig)
+		userRouterGroup.POST("/set_user_client_config", u.SetUserClientConfig)
+		userRouterGroup.POST("/del_user_client_config", u.DelUserClientConfig)
+		userRouterGroup.POST("/page_user_client_config", u.PageUserClientConfig)
 	}
 	// friend routing group
 	{
diff --git a/internal/api/user.go b/internal/api/user.go
index 6427e222e..93a311a9b 100644
--- a/internal/api/user.go
+++ b/internal/api/user.go
@@ -242,3 +242,19 @@ func (u *UserApi) UpdateNotificationAccountInfo(c *gin.Context) {
 func (u *UserApi) SearchNotificationAccount(c *gin.Context) {
 	a2r.Call(c, user.UserClient.SearchNotificationAccount, u.Client)
 }
+
+func (u *UserApi) GetUserClientConfig(c *gin.Context) {
+	a2r.Call(c, user.UserClient.GetUserClientConfig, u.Client)
+}
+
+func (u *UserApi) SetUserClientConfig(c *gin.Context) {
+	a2r.Call(c, user.UserClient.SetUserClientConfig, u.Client)
+}
+
+func (u *UserApi) DelUserClientConfig(c *gin.Context) {
+	a2r.Call(c, user.UserClient.DelUserClientConfig, u.Client)
+}
+
+func (u *UserApi) PageUserClientConfig(c *gin.Context) {
+	a2r.Call(c, user.UserClient.PageUserClientConfig, u.Client)
+}
diff --git a/internal/rpc/user/config.go b/internal/rpc/user/config.go
new file mode 100644
index 000000000..5a9a46359
--- /dev/null
+++ b/internal/rpc/user/config.go
@@ -0,0 +1,71 @@
+package user
+
+import (
+	"context"
+
+	"github.com/openimsdk/open-im-server/v3/pkg/authverify"
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
+	pbuser "github.com/openimsdk/protocol/user"
+	"github.com/openimsdk/tools/utils/datautil"
+)
+
+func (s *userServer) GetUserClientConfig(ctx context.Context, req *pbuser.GetUserClientConfigReq) (*pbuser.GetUserClientConfigResp, error) {
+	if req.UserID != "" {
+		if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil {
+			return nil, err
+		}
+		if _, err := s.db.GetUserByID(ctx, req.UserID); err != nil {
+			return nil, err
+		}
+	}
+	res, err := s.clientConfig.GetUserConfig(ctx, req.UserID)
+	if err != nil {
+		return nil, err
+	}
+	return &pbuser.GetUserClientConfigResp{Configs: res}, nil
+}
+
+func (s *userServer) SetUserClientConfig(ctx context.Context, req *pbuser.SetUserClientConfigReq) (*pbuser.SetUserClientConfigResp, error) {
+	if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil {
+		return nil, err
+	}
+	if req.UserID != "" {
+		if _, err := s.db.GetUserByID(ctx, req.UserID); err != nil {
+			return nil, err
+		}
+	}
+	if err := s.clientConfig.SetUserConfig(ctx, req.UserID, req.Configs); err != nil {
+		return nil, err
+	}
+	return &pbuser.SetUserClientConfigResp{}, nil
+}
+
+func (s *userServer) DelUserClientConfig(ctx context.Context, req *pbuser.DelUserClientConfigReq) (*pbuser.DelUserClientConfigResp, error) {
+	if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil {
+		return nil, err
+	}
+	if err := s.clientConfig.DelUserConfig(ctx, req.UserID, req.Keys); err != nil {
+		return nil, err
+	}
+	return &pbuser.DelUserClientConfigResp{}, nil
+}
+
+func (s *userServer) PageUserClientConfig(ctx context.Context, req *pbuser.PageUserClientConfigReq) (*pbuser.PageUserClientConfigResp, error) {
+	if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil {
+		return nil, err
+	}
+	total, res, err := s.clientConfig.GetUserConfigPage(ctx, req.UserID, req.Key, req.Pagination)
+	if err != nil {
+		return nil, err
+	}
+	return &pbuser.PageUserClientConfigResp{
+		Total: total,
+		Configs: datautil.Slice(res, func(e *model.ClientConfig) *pbuser.ClientConfig {
+			return &pbuser.ClientConfig{
+				UserID: e.UserID,
+				Key:    e.Key,
+				Value:  e.Value,
+			}
+		}),
+	}, nil
+}
diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go
index 3e8ec3537..6ef61f773 100644
--- a/internal/rpc/user/user.go
+++ b/internal/rpc/user/user.go
@@ -64,6 +64,7 @@ type userServer struct {
 	webhookClient            *webhook.Client
 	groupClient              *rpcli.GroupClient
 	relationClient           *rpcli.RelationClient
+	clientConfig             controller.ClientConfigDatabase
 }
 
 type Config struct {
@@ -98,6 +99,10 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr
 	if err != nil {
 		return err
 	}
+	clientConfigDB, err := mgo.NewClientConfig(mgocli.GetDB())
+	if err != nil {
+		return err
+	}
 	msgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)
 	if err != nil {
 		return err
@@ -122,9 +127,9 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr
 		userNotificationSender:   NewUserNotificationSender(config, msgClient, WithUserFunc(database.FindWithError)),
 		config:                   config,
 		webhookClient:            webhook.NewWebhookClient(config.WebhooksConfig.URL),
-
-		groupClient:    rpcli.NewGroupClient(groupConn),
-		relationClient: rpcli.NewRelationClient(friendConn),
+		clientConfig:             controller.NewClientConfigDatabase(clientConfigDB, redis.NewClientConfigCache(rdb, clientConfigDB), mgocli.GetTx()),
+		groupClient:              rpcli.NewGroupClient(groupConn),
+		relationClient:           rpcli.NewRelationClient(friendConn),
 	}
 	pbuser.RegisterUserServer(server, u)
 	return u.db.InitOnce(context.Background(), users)
diff --git a/pkg/common/storage/cache/cachekey/client_config.go b/pkg/common/storage/cache/cachekey/client_config.go
new file mode 100644
index 000000000..16770adef
--- /dev/null
+++ b/pkg/common/storage/cache/cachekey/client_config.go
@@ -0,0 +1,10 @@
+package cachekey
+
+const ClientConfig = "CLIENT_CONFIG"
+
+func GetClientConfigKey(userID string) string {
+	if userID == "" {
+		return ClientConfig
+	}
+	return ClientConfig + ":" + userID
+}
diff --git a/pkg/common/storage/cache/client_config.go b/pkg/common/storage/cache/client_config.go
new file mode 100644
index 000000000..329f25c59
--- /dev/null
+++ b/pkg/common/storage/cache/client_config.go
@@ -0,0 +1,8 @@
+package cache
+
+import "context"
+
+type ClientConfigCache interface {
+	DeleteUserCache(ctx context.Context, userIDs []string) error
+	GetUserConfig(ctx context.Context, userID string) (map[string]string, error)
+}
diff --git a/pkg/common/storage/cache/redis/client_config.go b/pkg/common/storage/cache/redis/client_config.go
new file mode 100644
index 000000000..c5a455146
--- /dev/null
+++ b/pkg/common/storage/cache/redis/client_config.go
@@ -0,0 +1,69 @@
+package redis
+
+import (
+	"context"
+	"time"
+
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache"
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey"
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
+	"github.com/redis/go-redis/v9"
+)
+
+func NewClientConfigCache(rdb redis.UniversalClient, mgo database.ClientConfig) cache.ClientConfigCache {
+	rc := newRocksCacheClient(rdb)
+	return &ClientConfigCache{
+		mgo:      mgo,
+		rcClient: rc,
+		delete:   rc.GetBatchDeleter(),
+	}
+}
+
+type ClientConfigCache struct {
+	mgo      database.ClientConfig
+	rcClient *rocksCacheClient
+	delete   cache.BatchDeleter
+}
+
+func (x *ClientConfigCache) getExpireTime(userID string) time.Duration {
+	if userID == "" {
+		return time.Hour * 24
+	} else {
+		return time.Hour
+	}
+}
+
+func (x *ClientConfigCache) getClientConfigKey(userID string) string {
+	return cachekey.GetClientConfigKey(userID)
+}
+
+func (x *ClientConfigCache) GetConfig(ctx context.Context, userID string) (map[string]string, error) {
+	return getCache(ctx, x.rcClient, x.getClientConfigKey(userID), x.getExpireTime(userID), func(ctx context.Context) (map[string]string, error) {
+		return x.mgo.Get(ctx, userID)
+	})
+}
+
+func (x *ClientConfigCache) DeleteUserCache(ctx context.Context, userIDs []string) error {
+	keys := make([]string, 0, len(userIDs))
+	for _, userID := range userIDs {
+		keys = append(keys, x.getClientConfigKey(userID))
+	}
+	return x.delete.ExecDelWithKeys(ctx, keys)
+}
+
+func (x *ClientConfigCache) GetUserConfig(ctx context.Context, userID string) (map[string]string, error) {
+	config, err := x.GetConfig(ctx, "")
+	if err != nil {
+		return nil, err
+	}
+	if userID != "" {
+		userConfig, err := x.GetConfig(ctx, userID)
+		if err != nil {
+			return nil, err
+		}
+		for k, v := range userConfig {
+			config[k] = v
+		}
+	}
+	return config, nil
+}
diff --git a/pkg/common/storage/controller/client_config.go b/pkg/common/storage/controller/client_config.go
new file mode 100644
index 000000000..1c3787634
--- /dev/null
+++ b/pkg/common/storage/controller/client_config.go
@@ -0,0 +1,58 @@
+package controller
+
+import (
+	"context"
+
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache"
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
+	"github.com/openimsdk/tools/db/pagination"
+	"github.com/openimsdk/tools/db/tx"
+)
+
+type ClientConfigDatabase interface {
+	SetUserConfig(ctx context.Context, userID string, config map[string]string) error
+	GetUserConfig(ctx context.Context, userID string) (map[string]string, error)
+	DelUserConfig(ctx context.Context, userID string, keys []string) error
+	GetUserConfigPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error)
+}
+
+func NewClientConfigDatabase(db database.ClientConfig, cache cache.ClientConfigCache, tx tx.Tx) ClientConfigDatabase {
+	return &clientConfigDatabase{
+		tx:    tx,
+		db:    db,
+		cache: cache,
+	}
+}
+
+type clientConfigDatabase struct {
+	tx    tx.Tx
+	db    database.ClientConfig
+	cache cache.ClientConfigCache
+}
+
+func (x *clientConfigDatabase) SetUserConfig(ctx context.Context, userID string, config map[string]string) error {
+	return x.tx.Transaction(ctx, func(ctx context.Context) error {
+		if err := x.db.Set(ctx, userID, config); err != nil {
+			return err
+		}
+		return x.cache.DeleteUserCache(ctx, []string{userID})
+	})
+}
+
+func (x *clientConfigDatabase) GetUserConfig(ctx context.Context, userID string) (map[string]string, error) {
+	return x.cache.GetUserConfig(ctx, userID)
+}
+
+func (x *clientConfigDatabase) DelUserConfig(ctx context.Context, userID string, keys []string) error {
+	return x.tx.Transaction(ctx, func(ctx context.Context) error {
+		if err := x.db.Del(ctx, userID, keys); err != nil {
+			return err
+		}
+		return x.cache.DeleteUserCache(ctx, []string{userID})
+	})
+}
+
+func (x *clientConfigDatabase) GetUserConfigPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) {
+	return x.db.GetPage(ctx, userID, key, pagination)
+}
diff --git a/pkg/common/storage/database/client_config.go b/pkg/common/storage/database/client_config.go
new file mode 100644
index 000000000..7fa888d24
--- /dev/null
+++ b/pkg/common/storage/database/client_config.go
@@ -0,0 +1,15 @@
+package database
+
+import (
+	"context"
+
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
+	"github.com/openimsdk/tools/db/pagination"
+)
+
+type ClientConfig interface {
+	Set(ctx context.Context, userID string, config map[string]string) error
+	Get(ctx context.Context, userID string) (map[string]string, error)
+	Del(ctx context.Context, userID string, keys []string) error
+	GetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error)
+}
diff --git a/pkg/common/storage/database/mgo/client_config.go b/pkg/common/storage/database/mgo/client_config.go
new file mode 100644
index 000000000..0aa462899
--- /dev/null
+++ b/pkg/common/storage/database/mgo/client_config.go
@@ -0,0 +1,99 @@
+// Copyright © 2023 OpenIM open source community. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mgo
+
+import (
+	"context"
+
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
+	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
+	"github.com/openimsdk/tools/db/mongoutil"
+	"github.com/openimsdk/tools/db/pagination"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+
+	"github.com/openimsdk/tools/errs"
+)
+
+func NewClientConfig(db *mongo.Database) (database.ClientConfig, error) {
+	coll := db.Collection("config")
+	_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{
+		{
+			Keys: bson.D{
+				{Key: "key", Value: 1},
+				{Key: "user_id", Value: 1},
+			},
+			Options: options.Index().SetUnique(true),
+		},
+	})
+	if err != nil {
+		return nil, errs.Wrap(err)
+	}
+	return &ClientConfig{
+		coll: coll,
+	}, nil
+}
+
+type ClientConfig struct {
+	coll *mongo.Collection
+}
+
+func (x *ClientConfig) Set(ctx context.Context, userID string, config map[string]string) error {
+	if len(config) == 0 {
+		return nil
+	}
+	for key, value := range config {
+		filter := bson.M{"key": key, "user_id": userID}
+		update := bson.M{
+			"value": value,
+		}
+		err := mongoutil.UpdateOne(ctx, x.coll, filter, bson.M{"$set": update}, false, options.Update().SetUpsert(true))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (x *ClientConfig) Get(ctx context.Context, userID string) (map[string]string, error) {
+	cs, err := mongoutil.Find[*model.ClientConfig](ctx, x.coll, bson.M{"user_id": userID})
+	if err != nil {
+		return nil, err
+	}
+	cm := make(map[string]string)
+	for _, config := range cs {
+		cm[config.Key] = config.Value
+	}
+	return cm, nil
+}
+
+func (x *ClientConfig) Del(ctx context.Context, userID string, keys []string) error {
+	if len(keys) == 0 {
+		return nil
+	}
+	return mongoutil.DeleteMany(ctx, x.coll, bson.M{"key": bson.M{"$in": keys}, "user_id": userID})
+}
+
+func (x *ClientConfig) GetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) {
+	filter := bson.M{}
+	if userID != "" {
+		filter["user_id"] = userID
+	}
+	if key != "" {
+		filter["key"] = key
+	}
+	return mongoutil.FindPage[*model.ClientConfig](ctx, x.coll, filter, pagination)
+}
diff --git a/pkg/common/storage/model/client_config.go b/pkg/common/storage/model/client_config.go
new file mode 100644
index 000000000..f06e29102
--- /dev/null
+++ b/pkg/common/storage/model/client_config.go
@@ -0,0 +1,7 @@
+package model
+
+type ClientConfig struct {
+	Key    string `bson:"key"`
+	UserID string `bson:"user_id"`
+	Value  string `bson:"value"`
+}