Browse OpenIMSDK Guides
Guides

Server Development

Extend OpenIMServer through API, RPC, and storage layers.

Copy

Extend OpenIMSDK

  • Before developing new capabilities for OpenIMSDK, determine whether the requirement belongs to the business extension layer or core IM logic.
  • OpenIMSDK provides comprehensive abstractions for standard instant messaging features; modifying core implementations directly is generally discouraged.
  • When you do need to extend core IM capabilities, follow the workflow described below and submit a pull request to the upstream repository to facilitate long-term maintenance.

OpenIMServer

OpenIMServer handles real-time message pipelines over persistent WebSocket connections (entry point in /internal/msggateway) and REST API business logic over short-lived HTTP connections (entry point in /internal/api/).

The following walkthrough uses adding an AddFriendCategory feature as an example to explain how to modify the API, RPC, and Storage layers in sequence.

Prerequisites

OpenIMServer imports protobuf protocol definitions from github.com/openimsdk/protocol. When you need to modify protocol definitions, fork this repository, add the new schema definitions, and point OpenIMServer to your fork in go.mod:

replace github.com/openimsdk/protocol => github.com/<your-username>/protocol

Add and generate Protobuf protocols

Define the Proto schema

The following example adds AddFriendCategory to relation/relation.proto in the Friend module:

syntax = "proto3";

package openim.relation;

option go_package = "github.com/openimsdk/protocol/relation";

message AddFriendCategoryReq {
  string ownerUserID = 1;
  string friendUserID = 2;
  int32 category = 3;
}

message AddFriendCategoryResp {
}

service Friend {
  rpc AddFriendCategory(AddFriendCategoryReq) returns (AddFriendCategoryResp);
}

This definition includes the request parameters, an empty response, and the new RPC method.

Generate Go code

  1. Install Mage. OpenIMServer standardizes development commands with Mage to eliminate cross-platform shell script discrepancies:
   go install github.com/magefile/mage@latest
  1. Run mage InstallDepend in the protocol repository to install required dependencies.
  2. Modify your proto files, then run mage GenGo to generate Go code.
  3. For additional details, refer to Generating PB files with Mage.

Add parameter validation

OpenIMServer avoids using proto tags and reflection plugins for parameter validation. Instead, add a Go file in the generated protobuf directory and implement a Check method on the request struct:

func (x *AddFriendCategoryReq) Check() error {
  if x.OwnerUserID == "" {
    return errors.New("OwnerUserID is empty")
  }
  if x.FriendUserID == "" {
    return errors.New("FriendUserID is empty")
  }
  if x.Category == 0 {
    return errors.New("Category is empty")
  }
  return nil
}

Add API endpoints

Define routes

Routes are defined in /internal/api/router.go. Register the new endpoint in the friend route group inside newGinRouter:

{
  f := NewFriendApi(relation.NewFriendClient(friendConn))
  friendRouterGroup := r.Group("/friend")
  friendRouterGroup.POST("/delete_friend", f.DeleteFriend)
  // ...
  friendRouterGroup.POST("/add_friend_category", f.AddFriendCategory)
}

If the endpoint belongs to an existing group, append it directly; otherwise, create a new route group file modeled after existing modules.

Implement the API handler

Implement the handler in /internal/api/friend/friend.go. If the incoming JSON payload perfectly matches the RPC request struct, invoke a2r.Call directly; otherwise, manually parse the request and invoke the gRPC client (refer to SendMessage in the Message module as a reference):

func (o *FriendApi) AddFriendCategory(c *gin.Context) {
  a2r.Call(c, relation.FriendClient.AddFriendCategory, o.client)
}

Add RPC methods

Implement the new gRPC method on the corresponding module's Server struct. When database updates or inserts require real-time synchronization with OpenIMClientSDK, follow the notification pattern used in s.notificationSender.FriendsInfoUpdateNotification.

Implement business logic

Implement AddFriendCategory in internal/rpc/relation/friend/friend.go:

func (s *friendServer) AddFriendCategory(
  ctx context.Context,
  req *relation.AddFriendCategoryReq,
) (*relation.AddFriendCategoryResp, error) {
  if err := authverify.CheckAccessV3(
    ctx,
    req.OwnerUserID,
    s.config.Share.IMAdminUserID,
  ); err != nil {
    return nil, err
  }

  if _, err := s.db.FindFriendsWithError(
    ctx,
    req.OwnerUserID,
    []string{req.FriendUserID},
  ); err != nil {
    return nil, err
  }

  if err := s.db.AddFriendCategory(
    ctx,
    req.OwnerUserID,
    req.FriendUserID,
    req.Category,
  ); err != nil {
    return nil, err
  }

  s.notification.FriendCategoryAddNotification(
    ctx,
    req.OwnerUserID,
    req.FriendUserID,
  )

  return &relation.AddFriendCategoryResp{}, nil
}

Implement the notification helper in internal/rpc/relation/notification.go:

func (f *FriendNotificationSender) FriendCategoryAddNotification(
  ctx context.Context,
  fromUserID,
  toUserID string,
) {
  tips := sdkws.FriendInfoChangedTips{
    FromToUserID: &sdkws.FromToUserID{},
  }
  tips.FromToUserID.FromUserID = fromUserID
  tips.FromToUserID.ToUserID = toUserID
  f.setSortVersion(
    ctx,
    &tips.FriendVersion,
    &tips.FriendVersionID,
    database.FriendVersionName,
    toUserID,
    &tips.FriendSortVersion,
  )
  f.Notification(
    ctx,
    fromUserID,
    toUserID,
    constant.FriendCategoryAddNotification,
    &tips,
  )
}

Register the notification type in constant/constant.go within your protocol repository:

const (
  FriendApplicationApprovedNotification = 1201
  // ...
  FriendCategoryAddNotification = 1211
)

Next, add the new field to FriendInfo in sdkws/sdkws.proto and regenerate sdkws/sdkws.pb.go:

message FriendInfo {
  string ownerUserID = 1;
  string remark = 2;
  // ...
  int32 category = 9;
}

Add Storage layer interfaces

The OpenIMServer Storage layer is organized into three distinct tiers:

  • Controller: Orchestrates transactions, cache invalidation, and domain model mapping.
  • Cache: Caches high-frequency data to reduce database load.
  • Database: Executes persistent data read and write operations.

Controller tier

Extend the interface and implement the method in pkg/common/storage/controller/friend.go:

type FriendDatabase interface {
  CheckIn(
    ctx context.Context,
    user1,
    user2 string,
  ) (inUser1Friends bool, inUser2Friends bool, err error)
  // ...
  AddFriendCategory(
    ctx context.Context,
    ownerUserID,
    friendUserID string,
    category int,
  ) error
}

func (f *FriendDatabase) AddFriendCategory(
  ctx context.Context,
  ownerUserID,
  friendUserID string,
  category int,
) error {
  if err := f.friend.AddFriendCategory(
    ctx,
    ownerUserID,
    friendUserID,
    category,
  ); err != nil {
    return err
  }

  return f.cache.
    DeleteFriend(ownerUserID, friendUserID).
    DelMaxFriendVersion(ownerUserID).
    ChainExecDel(ctx)
}

Cache tier

Declare cache interfaces in pkg/common/storage/cache, manage cache keys in pkg/common/storage/cache/cachekey, and provide the implementations required by the Controller tier. For this example, you can reuse the existing DeleteFriend cache method.

OpenIMServer follows a "delete on write, populate on read" caching strategy: invalidate the cached entry when the database updates, and re-cache data during subsequent reads.

Database tier

Add the new field to the database model in pkg/common/storage/model/friend.go:

type Friend struct {
  ID          primitive.ObjectID `bson:"_id"`
  OwnerUserID string             `bson:"owner_user_id"`
  // ...
  Category int `bson:"category"`
}

Extend the interface in pkg/common/storage/database/friend.go:

type Friend interface {
  UpdateRemark(
    ctx context.Context,
    ownerUserID,
    friendUserID,
    remark string,
  ) error
  // ...
  AddFriendCategory(
    ctx context.Context,
    ownerUserID,
    friendUserID string,
    category int,
  ) error
}

Finally, implement the database query in pkg/common/storage/database/mgo/friend.go:

func (f *FriendMgo) AddFriendCategory(
  ctx context.Context,
  ownerUserID,
  friendUserID string,
  category int,
) error {
  return f.UpdateByMap(
    ctx,
    ownerUserID,
    friendUserID,
    map[string]any{"category": category},
  )
}

OpenIMClientSDK

The cross-platform foundation of OpenIMClientSDK is OpenIMClientSDK Core. It implements core client-side IM capabilities:

  • Network connection lifecycle: Maintains stable, persistent WebSocket connections with OpenIMServer.
  • Message receipt and storage: Ingests incoming messages and persists them in the local SQLite database.
  • Relationship and group management: Manages friendship relations, blocklists, group profiles, and group member records.
  • Cross-platform parity: Delivers identical core behavior across Android, iOS, Windows, macOS, and Web platforms.

Define Server API callers

When the new client method requires invoking an OpenIMServer endpoint, register the API declaration in server_api.

Declare the API client in pkg/api/api.go:

var (
  AddFriend = newApi[
    relation.ApplyToAddFriendReq,
    relation.ApplyToAddFriendResp,
  ]("/friend/add_friend")
  // ...
  AddFriendCategory = newApi[
    relation.AddFriendCategoryReq,
    relation.AddFriendCategoryResp,
  ]("/friend/add_friend_category")
)

relation.AddFriendCategoryReq is imported from the shared protocol repository; OpenIMClientSDK Core depends on the identical protocol definitions.

Add the caller method in relation/server_api.go:

func (r *Relation) AddFriendCategory(
  ctx context.Context,
  req *relation.AddFriendCategoryReq,
) error {
  req.OwnerUserID = r.loginUserID
  return api.AddFriendCategory.Execute(ctx, req)
}

Implement SDK business logic

Implement the public SDK method in internal/relation/api.go. Requests and responses can use protobuf structs or custom structs with JSON tags:

func (r *Relation) AddFriendCategory(
  ctx context.Context,
  req *sdkpb.AddFriendCategoryReq,
) (*sdkpb.AddFriendCategoryResp, error) {
  serverReq := &relation.AddFriendCategoryReq{
    OwnerUserID:  r.loginUserID,
    FriendUserID: req.FriendUserID,
    Category:     req.Category,
  }
  if err := r.AddFriendCategory(ctx, serverReq); err != nil {
    return nil, err
  }

  r.relationSyncMutex.Lock()
  defer r.relationSyncMutex.Unlock()
  if err := r.IncrSyncFriends(ctx); err != nil {
    return nil, err
  }

  return &sdkpb.AddFriendCategoryResp{}, nil
}

Handle OpenIMServer notifications

Process server push notifications in internal/relation/notification.go:

func (r *Relation) doNotification(
  ctx context.Context,
  msg *sdkws.MsgData,
) error {
  r.relationSyncMutex.Lock()
  defer r.relationSyncMutex.Unlock()

  switch msg.ContentType {
  case constant.FriendRemarkSetNotification:
    // ...
  case constant.FriendCategoryAddNotification:
    var tips sdkws.FriendCategoryAddTips
    if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
      return err
    }
    if tips.FromToUserID != nil &&
      tips.FromToUserID.FromUserID == r.loginUserID {
      return r.IncrSyncFriends(ctx)
    }
  }
  return nil
}

Because IncrSyncFriends writes updated data into the local database, map the newly introduced field in internal/relation/conversion.go:

func ServerFriendToLocalFriend(
  info *sdkws.FriendInfo,
) *model_struct.LocalFriend {
  return &model_struct.LocalFriend{
    OwnerUserID:    info.OwnerUserID,
    FriendUserID:   info.FriendUser.UserID,
    Remark:         info.Remark,
    CreateTime:     info.CreateTime,
    AddSource:      info.AddSource,
    OperatorUserID: info.OperatorUserID,
    Nickname:       info.FriendUser.Nickname,
    FaceURL:        info.FriendUser.FaceURL,
    Ex:             info.Ex,
    IsPinned:       info.IsPinned,
    Category:       info.Category,
  }
}

Update the local database

  1. Extend the interface in pkg/db/db_interface/databse.go called by OpenIMClientSDK. For this example, reuse UpdateFriend.
  2. Add the field to LocalFriend in pkg/db/model_struct/data_model_struct.go:
type LocalFriend struct {
  OwnerUserID  string `gorm:"column:owner_user_id;primary_key;type:varchar(64)" json:"ownerUserID"`
  FriendUserID string `gorm:"column:friend_user_id;primary_key;type:varchar(64)" json:"userID"`
  Remark       string `gorm:"column:remark;type:varchar(255)" json:"remark"`
  // ...
  Category int32 `gorm:"column:category" json:"category"`
}
  1. Implement the persistence logic in pkg/db/friend_model.go.

Export cross-language bindings

OpenIMClientSDK Core is built with Go. Once the core logic is complete, export bindings for target client languages:

  • open_im_sdk/: Function interface binding layer.
  • open_im_sdk_callback/: Callback signature definition layer.

Android and iOS use gomobile to compile native AAR and xcframework artifacts. Refer to the OpenIMClientSDK Core repository for build instructions.

For example, export the method in open_im_sdk/relation.go:

func AddFriendCategory(
  callback open_im_sdk_callback.Base,
  operationID string,
  req string,
) {
  call(
    callback,
    operationID,
    UserForSDK.Relation().AddFriendCategory,
    req,
  )
}

OpenIMClientSDK Core also provides C bindings for other cross-platform integrations, maintained in the OpenIMClientSDK C++ repository. When integrating via C interfaces in other languages, refer to the wrapper patterns in that repository.