This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 4bfd002c feat: add cloud credential store, vendor-aware instances and 
instance provider routing (#1147)
4bfd002c is described below

commit 4bfd002c7c94cd08cf6dfee4193a54bd778aa55c
Author: lizhimins <[email protected]>
AuthorDate: Thu Aug 6 20:46:53 2026 +0800

    feat: add cloud credential store, vendor-aware instances and instance 
provider routing (#1147)
---
 deploy/mysql/upgrade-cloud-vendor.sql              |  89 +++++++++++
 .../rocketmq/studio/auth/AuthInterceptor.java      |   7 +-
 .../credential/CloudCredentialController.java      |  74 ++++++++++
 .../CloudCredentialDeleteRequestDTO.java}          |  25 ++--
 .../credential/CloudCredentialRepository.java}     |  20 +--
 .../cloud/credential/CloudCredentialService.java   | 149 +++++++++++++++++++
 .../studio/cloud/credential/CloudCredentialVO.java | 102 +++++++++++++
 .../cloud/credential/CreateCloudCredentialDTO.java |  98 +++++++++++++
 .../MybatisPlusCloudCredentialRepository.java      | 139 ++++++++++++++++++
 .../credential/UpdateCloudCredentialDTO.java}      |  49 +++++--
 .../domain/enums/InstanceVendor.java}              |  17 +--
 .../common/exception/GlobalExceptionHandler.java   |   7 +
 .../studio/instance/InstanceRepository.java        |   2 +
 .../rocketmq/studio/instance/InstanceService.java  | 131 +++++++++++++++--
 .../rocketmq/studio/instance/InstanceVO.java       |   5 +
 .../instance/MybatisPlusInstanceRepository.java    |  26 ++++
 .../rocketmq/studio/instance/dlq/DLQService.java   |  13 ++
 .../instance/group/ConsumerGroupController.java    |  26 ++--
 .../instance/group/CreateConsumerGroupDTO.java     |   3 +
 .../instance/group/DeleteConsumerGroupDTO.java     |   2 +
 .../instance/group/ResetConsumerOffsetDTO.java     |   2 +
 .../studio/instance/message/MessageService.java    |  10 +-
 .../studio/instance/topic/CreateTopicDTO.java      |   3 +
 .../studio/instance/topic/DeleteTopicDTO.java      |   2 +
 .../studio/instance/topic/MetadataService.java     | 124 ++++++++++++++--
 .../studio/instance/topic/SendMessageDTO.java      |   2 +
 .../studio/instance/topic/TopicController.java     |  17 ++-
 .../persistence/entity/RmqCloudCredential.java     | 108 ++++++++++++++
 .../studio/persistence/entity/RmqInstance.java     |   8 +
 .../mapper/RmqCloudCredentialMapper.java}          |  17 +--
 .../CloudCatalogProvider.java}                     |  25 ++--
 .../studio/provider/CloudInstanceDetailVO.java     | 106 ++++++++++++++
 .../studio/provider/CloudInstanceOptionVO.java     |  84 +++++++++++
 .../CloudRegionVO.java}                            |  41 +++---
 .../rocketmq/studio/provider/InstanceProvider.java |  65 ++++++++
 .../studio/provider/InstanceProviderRegistry.java  |  74 ++++++++++
 .../provider/apache/ApacheInstanceProvider.java    | 136 +++++++++++++++++
 .../provider/tencent/TencentInstanceProvider.java  | 110 ++++++++++++++
 server/src/main/resources/db/schema.sql            |  17 +++
 .../credential/CloudCredentialServiceTest.java     | 163 +++++++++++++++++++++
 .../studio/instance/InstanceServiceTest.java       | 108 ++++++++++++++
 .../studio/instance/dlq/DLQServiceTest.java        |   4 +
 .../group/ConsumerGroupControllerTest.java         |   5 +-
 .../instance/message/MessageServiceTest.java       |   7 +-
 .../studio/instance/topic/MetadataServiceTest.java |   4 +
 .../studio/instance/topic/TopicControllerTest.java |   8 +-
 .../provider/InstanceProviderRegistryTest.java     | 109 ++++++++++++++
 .../tencent/TencentInstanceProviderTest.java       |  65 ++++++++
 48 files changed, 2255 insertions(+), 153 deletions(-)

diff --git a/deploy/mysql/upgrade-cloud-vendor.sql 
b/deploy/mysql/upgrade-cloud-vendor.sql
new file mode 100644
index 00000000..8286a1bb
--- /dev/null
+++ b/deploy/mysql/upgrade-cloud-vendor.sql
@@ -0,0 +1,89 @@
+-- deploy/mysql/upgrade-cloud-vendor.sql
+-- 存量 MySQL 数据卷增量迁移:rmq_instance 增加厂商维度 + 新增 rmq_cloud_credential 凭据表
+-- 适用:数据卷已初始化、docker-entrypoint-initdb.d 不会再执行的存量部署。
+-- 全新数据卷由 server/src/main/resources/db/schema.sql 直接覆盖,无需本脚本。
+-- 幂等:可重复执行。
+--
+-- 用法(远程容器内执行):
+--   docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 
rocketmq_studio < upgrade-cloud-vendor.sql
+
+-- 固定连接编码,防止 mysql 客户端以 latin1 解释 UTF-8 字节导致中文双重编码
+SET NAMES utf8mb4;
+
+SET @schema_name := DATABASE();
+
+-- 1. rmq_instance.vendor
+SET @vendor_column_exists := (
+    SELECT COUNT(*)
+    FROM information_schema.columns
+    WHERE table_schema = @schema_name
+      AND table_name = 'rmq_instance'
+      AND column_name = 'vendor'
+);
+SET @vendor_sql := IF(@vendor_column_exists = 0,
+    "ALTER TABLE rmq_instance ADD COLUMN vendor VARCHAR(32) NOT NULL DEFAULT 
'APACHE' COMMENT 'APACHE/ALIYUN/TENCENT' AFTER endpoint",
+    'SELECT 1');
+PREPARE vendor_statement FROM @vendor_sql;
+EXECUTE vendor_statement;
+DEALLOCATE PREPARE vendor_statement;
+
+-- 2. rmq_instance.cloud_instance_id
+SET @cloud_instance_id_column_exists := (
+    SELECT COUNT(*)
+    FROM information_schema.columns
+    WHERE table_schema = @schema_name
+      AND table_name = 'rmq_instance'
+      AND column_name = 'cloud_instance_id'
+);
+SET @cloud_instance_id_sql := IF(@cloud_instance_id_column_exists = 0,
+    "ALTER TABLE rmq_instance ADD COLUMN cloud_instance_id VARCHAR(128) 
COMMENT '云厂商实例 ID(vendor 非 APACHE 时必填)' AFTER vendor",
+    'SELECT 1');
+PREPARE cloud_instance_id_statement FROM @cloud_instance_id_sql;
+EXECUTE cloud_instance_id_statement;
+DEALLOCATE PREPARE cloud_instance_id_statement;
+
+-- 3. rmq_instance.credential_id
+SET @credential_id_column_exists := (
+    SELECT COUNT(*)
+    FROM information_schema.columns
+    WHERE table_schema = @schema_name
+      AND table_name = 'rmq_instance'
+      AND column_name = 'credential_id'
+);
+SET @credential_id_sql := IF(@credential_id_column_exists = 0,
+    "ALTER TABLE rmq_instance ADD COLUMN credential_id VARCHAR(64) COMMENT '引用 
rmq_cloud_credential.id' AFTER cloud_instance_id",
+    'SELECT 1');
+PREPARE credential_id_statement FROM @credential_id_sql;
+EXECUTE credential_id_statement;
+DEALLOCATE PREPARE credential_id_statement;
+
+-- 4. rmq_instance.region_id
+SET @region_id_column_exists := (
+    SELECT COUNT(*)
+    FROM information_schema.columns
+    WHERE table_schema = @schema_name
+      AND table_name = 'rmq_instance'
+      AND column_name = 'region_id'
+);
+SET @region_id_sql := IF(@region_id_column_exists = 0,
+    "ALTER TABLE rmq_instance ADD COLUMN region_id VARCHAR(64) COMMENT '云 
region' AFTER credential_id",
+    'SELECT 1');
+PREPARE region_id_statement FROM @region_id_sql;
+EXECUTE region_id_statement;
+DEALLOCATE PREPARE region_id_statement;
+
+-- 5. 存量实例兜底回填
+UPDATE rmq_instance SET vendor = 'APACHE' WHERE vendor IS NULL OR vendor = '';
+
+-- 6. 云厂商凭据表(secret_key 为 base64 编码,禁止明文;access_key 明文用于唯一键与打码展示)
+CREATE TABLE IF NOT EXISTS rmq_cloud_credential (
+  id VARCHAR(64) PRIMARY KEY,
+  name VARCHAR(128) NOT NULL COMMENT '凭据显示名',
+  vendor VARCHAR(32) NOT NULL COMMENT 'ALIYUN/TENCENT',
+  access_key VARCHAR(255) NOT NULL,
+  secret_key VARCHAR(512) NOT NULL COMMENT 'base64 编码的 SK',
+  remark VARCHAR(255),
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_vendor_access_key (vendor, access_key)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
index f642319b..293b933c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
@@ -98,7 +98,12 @@ public class AuthInterceptor implements HandlerInterceptor {
     }
 
     private boolean isAdminOnlyGetPath(String path) {
-        return path.startsWith("/api/acl/users/") && 
path.endsWith("/credentials");
+        return isCredentialRevealPath(path, "/api/acl/users/")
+                || isCredentialRevealPath(path, "/api/cloud-credentials/");
+    }
+
+    private boolean isCredentialRevealPath(String path, String prefix) {
+        return path.startsWith(prefix) && path.endsWith("/credentials");
     }
 
     private void writeError(HttpServletResponse response, HttpStatus status, 
String message)
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialController.java
new file mode 100644
index 00000000..540cf116
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialController.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.cloud.credential;
+
+import jakarta.validation.Valid;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/cloud-credentials")
+public class CloudCredentialController {
+
+    private final CloudCredentialService credentialService;
+
+    public CloudCredentialController(CloudCredentialService credentialService) 
{
+        this.credentialService = credentialService;
+    }
+
+    @GetMapping
+    public Result<List<CloudCredentialVO>> listCredentials() {
+        return Result.ok(credentialService.listMasked());
+    }
+
+    @PostMapping("/create")
+    public Result<CloudCredentialVO> createCredential(
+            @Valid @RequestBody(required = false) CreateCloudCredentialDTO 
request) {
+        if (request == null) {
+            throw new BusinessException(400, "Cloud credential request is 
required");
+        }
+        return 
Result.ok(credentialService.create(request.toCloudCredentialVO()));
+    }
+
+    @PostMapping("/update")
+    public Result<CloudCredentialVO> updateCredential(
+            @Valid @RequestBody(required = false) UpdateCloudCredentialDTO 
request) {
+        if (request == null) {
+            throw new BusinessException(400, "Cloud credential request is 
required");
+        }
+        return Result.ok(credentialService.update(request));
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> deleteCredential(@Valid @RequestBody 
CloudCredentialDeleteRequestDTO request) {
+        credentialService.delete(request.getId());
+        return Result.ok();
+    }
+
+    @GetMapping("/{id}/credentials")
+    public Result<CloudCredentialVO> getCredentialSecrets(@PathVariable String 
id) {
+        return Result.ok(credentialService.reveal(id));
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialDeleteRequestDTO.java
similarity index 73%
copy from 
server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialDeleteRequestDTO.java
index 50f256c7..d4513327 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialDeleteRequestDTO.java
@@ -14,19 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.instance.topic;
+package org.apache.rocketmq.studio.cloud.credential;
 
 import jakarta.validation.constraints.NotBlank;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
 
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class DeleteTopicDTO {
-    @NotBlank(message = "name is required")
-    private String name;
+public class CloudCredentialDeleteRequestDTO {
+
+    @NotBlank(message = "credential id is required")
+    private String id;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialRepository.java
similarity index 64%
copy from 
server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialRepository.java
index 53410e6f..94e5d4fd 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialRepository.java
@@ -14,26 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.rocketmq.studio.cloud.credential;
 
-package org.apache.rocketmq.studio.instance;
-
-import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 
 import java.util.List;
 import java.util.Optional;
 
-public interface InstanceRepository {
-    List<InstanceVO> findAll();
-
-    List<InstanceVO> findByType(InstanceType type);
+public interface CloudCredentialRepository {
 
-    List<InstanceVO> search(String keyword);
+    List<CloudCredentialVO> findAll();
 
-    List<InstanceVO> findByTypeAndSearch(InstanceType type, String keyword);
+    Optional<CloudCredentialVO> findById(String id);
 
-    Optional<InstanceVO> findById(String id);
+    Optional<CloudCredentialVO> findByVendorAndAccessKey(InstanceVendor 
vendor, String accessKey);
 
-    InstanceVO save(InstanceVO instance);
+    CloudCredentialVO save(CloudCredentialVO credential);
 
-    void deleteById(String id);
+    boolean deleteById(String id);
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialService.java
new file mode 100644
index 00000000..c1418344
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialService.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.cloud.credential;
+
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class CloudCredentialService {
+
+    private static final Logger log = 
LoggerFactory.getLogger(CloudCredentialService.class);
+
+    private static final int VISIBLE_CREDENTIAL_CHARS = 4;
+    private static final int MIN_PARTIALLY_MASKED_CREDENTIAL_CHARS = 17;
+    private static final String CREDENTIAL_MASK = "****";
+
+    private final CloudCredentialRepository credentialRepository;
+    private final InstanceRepository instanceRepository;
+
+    public CloudCredentialService(CloudCredentialRepository 
credentialRepository,
+                                  InstanceRepository instanceRepository) {
+        this.credentialRepository = credentialRepository;
+        this.instanceRepository = instanceRepository;
+    }
+
+    public List<CloudCredentialVO> listMasked() {
+        log.info("Listing cloud credentials (masked)");
+        return credentialRepository.findAll().stream()
+                .map(this::maskAccessKey)
+                .toList();
+    }
+
+    public CloudCredentialVO create(CloudCredentialVO credential) {
+        if (credential == null) {
+            throw new BusinessException(400, "Cloud credential request is 
required");
+        }
+        if (isBlank(credential.getName())) {
+            throw new BusinessException(400, "Cloud credential name is 
required");
+        }
+        if (credential.getVendor() == null || credential.getVendor() == 
org.apache.rocketmq.studio.common.domain.enums.InstanceVendor.APACHE) {
+            throw new BusinessException(400, "Cloud credential vendor must be 
ALIYUN or TENCENT");
+        }
+        if (isBlank(credential.getAccessKey()) || 
isBlank(credential.getSecretKey())) {
+            throw new BusinessException(400, "Cloud credential accessKey and 
secretKey are required");
+        }
+        credentialRepository.findByVendorAndAccessKey(credential.getVendor(), 
credential.getAccessKey())
+                .ifPresent(existing -> {
+                    throw new BusinessException(400,
+                            "Cloud credential already exists for vendor " + 
credential.getVendor()
+                                    + " and accessKey " + 
maskCredential(credential.getAccessKey()));
+                });
+        log.info("Creating cloud credential name={}, vendor={}", 
credential.getName(), credential.getVendor());
+        credential.setId(UUID.randomUUID().toString());
+        credential.setCreatedAt(LocalDateTime.now());
+        credential.setUpdatedAt(LocalDateTime.now());
+        return maskAccessKey(credentialRepository.save(credential));
+    }
+
+    public CloudCredentialVO update(UpdateCloudCredentialDTO request) {
+        if (request == null || isBlank(request.getId())) {
+            throw new BusinessException(400, "Cloud credential id is 
required");
+        }
+        log.info("Updating cloud credential id={}", request.getId());
+        CloudCredentialVO existing = 
credentialRepository.findById(request.getId())
+                .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + request.getId()));
+        if (request.getName() != null && request.getName().isBlank()) {
+            throw new BusinessException(400, "Cloud credential name cannot be 
blank");
+        }
+        if (request.getName() != null) {
+            existing.setName(request.getName());
+        }
+        if (request.getSecretKey() != null && 
!request.getSecretKey().isBlank()) {
+            existing.setSecretKey(request.getSecretKey());
+        }
+        if (request.getRemark() != null) {
+            existing.setRemark(request.getRemark());
+        }
+        existing.setUpdatedAt(LocalDateTime.now());
+        return maskAccessKey(credentialRepository.save(existing));
+    }
+
+    public void delete(String id) {
+        if (isBlank(id)) {
+            throw new BusinessException(400, "Cloud credential id is 
required");
+        }
+        log.info("Deleting cloud credential id={}", id);
+        credentialRepository.findById(id)
+                .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + id));
+        if (instanceRepository.existsByCredentialId(id)) {
+            throw new BusinessException(400, "Cloud credential is referenced 
by existing instances");
+        }
+        credentialRepository.deleteById(id);
+    }
+
+    public CloudCredentialVO reveal(String id) {
+        if (isBlank(id)) {
+            throw new BusinessException(400, "Cloud credential id is 
required");
+        }
+        return credentialRepository.findById(id)
+                .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + id));
+    }
+
+    private CloudCredentialVO maskAccessKey(CloudCredentialVO credential) {
+        CloudCredentialVO masked = new CloudCredentialVO();
+        masked.setId(credential.getId());
+        masked.setName(credential.getName());
+        masked.setVendor(credential.getVendor());
+        masked.setAccessKey(maskCredential(credential.getAccessKey()));
+        masked.setSecretKey(null);
+        masked.setRemark(credential.getRemark());
+        masked.setCreatedAt(credential.getCreatedAt());
+        masked.setUpdatedAt(credential.getUpdatedAt());
+        return masked;
+    }
+
+    static String maskCredential(String value) {
+        if (value == null || value.length() < 
MIN_PARTIALLY_MASKED_CREDENTIAL_CHARS) {
+            return CREDENTIAL_MASK;
+        }
+        return value.substring(0, VISIBLE_CREDENTIAL_CHARS)
+                + CREDENTIAL_MASK
+                + value.substring(value.length() - VISIBLE_CREDENTIAL_CHARS);
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.isBlank();
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialVO.java
new file mode 100644
index 00000000..275f29b2
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialVO.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.cloud.credential;
+
+import org.apache.rocketmq.studio.common.domain.BaseEntity;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+
+import java.util.Objects;
+
+public class CloudCredentialVO extends BaseEntity {
+
+    private String name;
+    private InstanceVendor vendor;
+    private String accessKey;
+    private String secretKey;
+    private String remark;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public InstanceVendor getVendor() {
+        return vendor;
+    }
+
+    public void setVendor(InstanceVendor vendor) {
+        this.vendor = vendor;
+    }
+
+    public String getAccessKey() {
+        return accessKey;
+    }
+
+    public void setAccessKey(String accessKey) {
+        this.accessKey = accessKey;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof CloudCredentialVO)) {
+            return false;
+        }
+        CloudCredentialVO that = (CloudCredentialVO) o;
+        return Objects.equals(getId(), that.getId())
+                && Objects.equals(name, that.name)
+                && vendor == that.vendor
+                && Objects.equals(accessKey, that.accessKey)
+                && Objects.equals(secretKey, that.secretKey)
+                && Objects.equals(remark, that.remark)
+                && Objects.equals(getCreatedAt(), that.getCreatedAt())
+                && Objects.equals(getUpdatedAt(), that.getUpdatedAt());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getId(), name, vendor, accessKey, secretKey, 
remark,
+                getCreatedAt(), getUpdatedAt());
+    }
+
+    @Override
+    public String toString() {
+        return "CloudCredentialVO{id=" + getId() + ", name=" + name + ", 
vendor=" + vendor
+                + ", accessKey=" + accessKey + ", secretKey=****}";
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CreateCloudCredentialDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CreateCloudCredentialDTO.java
new file mode 100644
index 00000000..79856bb8
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/CreateCloudCredentialDTO.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.cloud.credential;
+
+import jakarta.validation.constraints.NotBlank;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+
+public class CreateCloudCredentialDTO {
+
+    @NotBlank(message = "credential name is required")
+    private String name;
+
+    @NotBlank(message = "credential vendor is required")
+    private String vendor;
+
+    @NotBlank(message = "credential accessKey is required")
+    private String accessKey;
+
+    @NotBlank(message = "credential secretKey is required")
+    private String secretKey;
+
+    private String remark;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getVendor() {
+        return vendor;
+    }
+
+    public void setVendor(String vendor) {
+        this.vendor = vendor;
+    }
+
+    public String getAccessKey() {
+        return accessKey;
+    }
+
+    public void setAccessKey(String accessKey) {
+        this.accessKey = accessKey;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public CloudCredentialVO toCloudCredentialVO() {
+        CloudCredentialVO vo = new CloudCredentialVO();
+        vo.setName(name);
+        vo.setVendor(parseVendor(vendor));
+        vo.setAccessKey(accessKey);
+        vo.setSecretKey(secretKey);
+        vo.setRemark(remark);
+        return vo;
+    }
+
+    static InstanceVendor parseVendor(String vendor) {
+        if (vendor == null) {
+            return null;
+        }
+        try {
+            return InstanceVendor.valueOf(vendor.trim().toUpperCase());
+        } catch (IllegalArgumentException ex) {
+            return null;
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/MybatisPlusCloudCredentialRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/MybatisPlusCloudCredentialRepository.java
new file mode 100644
index 00000000..c83b7352
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/MybatisPlusCloudCredentialRepository.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.cloud.credential;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.persistence.entity.RmqCloudCredential;
+import org.apache.rocketmq.studio.persistence.mapper.RmqCloudCredentialMapper;
+import org.springframework.stereotype.Repository;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * MySQL-backed cloud credential repository. Secret keys are stored 
base64-encoded in
+ * {@code rmq_cloud_credential.secret_key} and decoded when read; plain text 
is never persisted.
+ */
+@Repository
+public class MybatisPlusCloudCredentialRepository implements 
CloudCredentialRepository {
+
+    private final RmqCloudCredentialMapper credentialMapper;
+
+    public MybatisPlusCloudCredentialRepository(RmqCloudCredentialMapper 
credentialMapper) {
+        this.credentialMapper = credentialMapper;
+    }
+
+    @Override
+    public List<CloudCredentialVO> findAll() {
+        return credentialMapper.selectList(
+                        new 
QueryWrapper<RmqCloudCredential>().orderByAsc("id")).stream()
+                .map(MybatisPlusCloudCredentialRepository::toVO)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Optional<CloudCredentialVO> findById(String id) {
+        return Optional.ofNullable(credentialMapper.selectById(id))
+                .map(MybatisPlusCloudCredentialRepository::toVO);
+    }
+
+    @Override
+    public Optional<CloudCredentialVO> findByVendorAndAccessKey(InstanceVendor 
vendor, String accessKey) {
+        RmqCloudCredential entity = credentialMapper.selectOne(
+                new QueryWrapper<RmqCloudCredential>()
+                        .eq("vendor", vendor.name())
+                        .eq("access_key", accessKey)
+                        .last("LIMIT 1"));
+        return 
Optional.ofNullable(entity).map(MybatisPlusCloudCredentialRepository::toVO);
+    }
+
+    @Override
+    public CloudCredentialVO save(CloudCredentialVO credential) {
+        RmqCloudCredential entity = toEntity(credential);
+        if (credentialMapper.selectById(entity.getId()) != null) {
+            credentialMapper.updateById(entity);
+        } else {
+            credentialMapper.insert(entity);
+        }
+        return credential;
+    }
+
+    @Override
+    public boolean deleteById(String id) {
+        return credentialMapper.deleteById(id) > 0;
+    }
+
+    // ── Mapping ────────────────────────────────────────────────────
+
+    private static CloudCredentialVO toVO(RmqCloudCredential entity) {
+        CloudCredentialVO vo = new CloudCredentialVO();
+        vo.setId(entity.getId());
+        vo.setName(entity.getName());
+        vo.setVendor(parseVendor(entity.getVendor()));
+        vo.setAccessKey(entity.getAccessKey());
+        vo.setSecretKey(decodeBase64(entity.getSecretKey()));
+        vo.setRemark(entity.getRemark());
+        vo.setCreatedAt(entity.getCreatedAt());
+        vo.setUpdatedAt(entity.getUpdatedAt());
+        return vo;
+    }
+
+    private static RmqCloudCredential toEntity(CloudCredentialVO vo) {
+        RmqCloudCredential entity = new RmqCloudCredential();
+        entity.setId(vo.getId());
+        entity.setName(vo.getName());
+        entity.setVendor(vo.getVendor() == null ? null : 
vo.getVendor().name());
+        entity.setAccessKey(vo.getAccessKey());
+        entity.setSecretKey(encodeBase64(vo.getSecretKey()));
+        entity.setRemark(vo.getRemark());
+        entity.setCreatedAt(vo.getCreatedAt());
+        entity.setUpdatedAt(vo.getUpdatedAt() == null ? LocalDateTime.now() : 
vo.getUpdatedAt());
+        return entity;
+    }
+
+    private static InstanceVendor parseVendor(String vendor) {
+        try {
+            return InstanceVendor.valueOf(vendor);
+        } catch (IllegalArgumentException | NullPointerException ex) {
+            return null;
+        }
+    }
+
+    static String encodeBase64(String plainText) {
+        if (plainText == null) {
+            return null;
+        }
+        return 
Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
+    }
+
+    static String decodeBase64(String stored) {
+        if (stored == null) {
+            return null;
+        }
+        try {
+            return new String(Base64.getDecoder().decode(stored), 
StandardCharsets.UTF_8);
+        } catch (IllegalArgumentException ex) {
+            // tolerate legacy values that were stored without encoding
+            return stored;
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/UpdateCloudCredentialDTO.java
similarity index 53%
copy from 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/cloud/credential/UpdateCloudCredentialDTO.java
index 7c4cdbfd..067c58f6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cloud/credential/UpdateCloudCredentialDTO.java
@@ -14,31 +14,50 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.persistence.entity;
+package org.apache.rocketmq.studio.cloud.credential;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
+import jakarta.validation.constraints.NotBlank;
 
-import java.time.LocalDateTime;
+public class UpdateCloudCredentialDTO {
 
-@Data
-@TableName("rmq_instance")
-public class RmqInstance {
-
-    @TableId(type = IdType.INPUT)
+    @NotBlank(message = "credential id is required")
     private String id;
 
     private String name;
 
+    private String secretKey;
+
     private String remark;
 
-    private String type;
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
 
-    private String endpoint;
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
 
-    private LocalDateTime createdAt;
+    public String getRemark() {
+        return remark;
+    }
 
-    private LocalDateTime updatedAt;
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceVendor.java
similarity index 68%
copy from 
server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceVendor.java
index 50f256c7..01ede90b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/domain/enums/InstanceVendor.java
@@ -14,19 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.instance.topic;
+package org.apache.rocketmq.studio.common.domain.enums;
 
-import jakarta.validation.constraints.NotBlank;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class DeleteTopicDTO {
-    @NotBlank(message = "name is required")
-    private String name;
+public enum InstanceVendor {
+    APACHE, ALIYUN, TENCENT
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
index ca390d77..98297fc5 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
@@ -43,6 +43,13 @@ public class GlobalExceptionHandler {
                 .body(Result.error(ex.getCode(), ex.getMessage()));
     }
 
+    @ExceptionHandler(UnsupportedOperationException.class)
+    @ResponseStatus(HttpStatus.NOT_IMPLEMENTED)
+    public Result<?> 
handleUnsupportedOperationException(UnsupportedOperationException ex) {
+        log.warn("Unsupported operation: {}", ex.getMessage());
+        return Result.error(HttpStatus.NOT_IMPLEMENTED.value(), 
ex.getMessage());
+    }
+
     @ExceptionHandler(PrometheusException.class)
     public ResponseEntity<Result<?>> 
handlePrometheusException(PrometheusException ex) {
         log.warn("Prometheus exception: status={}, message={}", 
ex.getStatusCode(), ex.getMessage());
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
index 53410e6f..f6c41f4d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
@@ -36,4 +36,6 @@ public interface InstanceRepository {
     InstanceVO save(InstanceVO instance);
 
     void deleteById(String id);
+
+    boolean existsByCredentialId(String credentialId);
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
index b95b48f2..18b33e9e 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
@@ -17,8 +17,13 @@
 
 package org.apache.rocketmq.studio.instance;
 
+import org.apache.rocketmq.studio.cloud.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.cloud.credential.CloudCredentialVO;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -33,36 +38,127 @@ import java.util.UUID;
 public class InstanceService {
 
     private final InstanceRepository instanceRepository;
+    private final CloudCredentialRepository cloudCredentialRepository;
+    private final InstanceProviderRegistry providerRegistry;
 
     public List<InstanceVO> listInstances(InstanceType type, String search) {
         log.debug("Listing instances, type={}, search={}", type, search);
         String normalizedSearch = search == null || search.isBlank() ? null : 
search.trim();
 
+        List<InstanceVO> instances;
         if (type != null && normalizedSearch != null) {
-            return instanceRepository.findByTypeAndSearch(type, 
normalizedSearch);
+            instances = instanceRepository.findByTypeAndSearch(type, 
normalizedSearch);
         } else if (type != null) {
-            return instanceRepository.findByType(type);
+            instances = instanceRepository.findByType(type);
         } else if (normalizedSearch != null) {
-            return instanceRepository.search(normalizedSearch);
+            instances = instanceRepository.search(normalizedSearch);
+        } else {
+            instances = instanceRepository.findAll();
+        }
+        instances.forEach(this::fillCloudCounts);
+        return instances;
+    }
+
+    /**
+     * Cloud instances keep topics/groups on the vendor side, so the local 
table counts are
+     * always zero; pull live counts through the vendor provider instead.
+     */
+    private void fillCloudCounts(InstanceVO instance) {
+        if (instance.getVendor() == null || instance.getVendor() == 
InstanceVendor.APACHE) {
+            return;
+        }
+        try {
+            org.apache.rocketmq.studio.provider.InstanceProvider provider =
+                    providerRegistry.forVendor(instance.getVendor());
+            instance.setTopicCount(provider.listTopics(instance.getId(), null, 
null).size());
+            
instance.setConsumerGroupCount(provider.listConsumerGroups(instance.getId(), 
null).size());
+        } catch (RuntimeException ex) {
+            log.warn("Failed to load cloud resource counts for instance {}: 
{}",
+                    instance.getId(), ex.getMessage());
         }
-        return instanceRepository.findAll();
     }
 
     public InstanceVO createInstance(InstanceVO instance) {
         requireInstance(instance);
-        log.info("Creating instance: {}", instance.getName());
+        InstanceVendor vendor = instance.getVendor() == null ? 
InstanceVendor.APACHE : instance.getVendor();
+        log.info("Creating instance: name={}, vendor={}", instance.getName(), 
vendor);
+
+        switch (vendor) {
+            case APACHE -> createApacheInstance(instance);
+            case ALIYUN -> createAliyunInstance(instance);
+            case TENCENT -> throw new BusinessException(501, "Tencent Cloud 
instance is not supported yet");
+        }
+
+        instance.setId(UUID.randomUUID().toString());
+        instance.setCreatedAt(LocalDateTime.now());
+        instance.setUpdatedAt(LocalDateTime.now());
+        return instanceRepository.save(instance);
+    }
 
+    private void createApacheInstance(InstanceVO instance) {
+        instance.setVendor(InstanceVendor.APACHE);
         if (instance.getName() == null || instance.getName().isBlank()) {
             throw new BusinessException(400, "InstanceVO name is required");
         }
         if (instance.getEndpoint() == null || 
instance.getEndpoint().isBlank()) {
             throw new BusinessException(400, "InstanceVO endpoint is 
required");
         }
+    }
 
-        instance.setId(UUID.randomUUID().toString());
-        instance.setCreatedAt(LocalDateTime.now());
-        instance.setUpdatedAt(LocalDateTime.now());
-        return instanceRepository.save(instance);
+    /**
+     * Commercial instances are never created manually: the user picks a 
stored credential and
+     * one of the cloud instances returned by the vendor catalog; endpoint is 
resolved from the
+     * cloud instance detail (VPC endpoint preferred).
+     */
+    private void createAliyunInstance(InstanceVO instance) {
+        instance.setVendor(InstanceVendor.ALIYUN);
+        if (instance.getEndpoint() != null && 
!instance.getEndpoint().isBlank()) {
+            throw new BusinessException(400, "Commercial instances must be 
selected from the cloud catalog, endpoint cannot be set manually");
+        }
+        if (isBlank(instance.getCredentialId()) || 
isBlank(instance.getCloudInstanceId())
+                || isBlank(instance.getRegionId())) {
+            throw new BusinessException(400, "credentialId, cloudInstanceId 
and regionId are required for Aliyun instances");
+        }
+        CloudCredentialVO credential = 
cloudCredentialRepository.findById(instance.getCredentialId())
+                .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + instance.getCredentialId()));
+        if (credential.getVendor() != InstanceVendor.ALIYUN) {
+            throw new BusinessException(400, "Cloud credential vendor does not 
match ALIYUN");
+        }
+        CloudInstanceDetailVO detail = 
providerRegistry.catalogFor(InstanceVendor.ALIYUN)
+                .getCloudInstance(instance.getCredentialId(), 
instance.getRegionId(), instance.getCloudInstanceId());
+        if (instance.getName() == null || instance.getName().isBlank()) {
+            instance.setName(detail.getInstanceName() != null && 
!detail.getInstanceName().isBlank()
+                    ? detail.getInstanceName() : detail.getInstanceId());
+        }
+        instance.setType(InstanceType.PROXY);
+        instance.setEndpoint(resolveEndpoint(detail));
+    }
+
+    private String resolveEndpoint(CloudInstanceDetailVO detail) {
+        if (detail.getEndpoints() == null || detail.getEndpoints().isEmpty()) {
+            throw new BusinessException(502, "Cloud instance has no endpoint: 
" + detail.getInstanceId());
+        }
+        return detail.getEndpoints().stream()
+                .filter(endpoint -> endpoint.getEndpointUrl() != null && 
!endpoint.getEndpointUrl().isBlank())
+                .sorted((a, b) -> 
Integer.compare(endpointPriority(a.getEndpointType()), 
endpointPriority(b.getEndpointType())))
+                .map(CloudInstanceDetailVO.CloudEndpoint::getEndpointUrl)
+                .findFirst()
+                .orElseThrow(() -> new BusinessException(502, "Cloud instance 
has no usable endpoint: " + detail.getInstanceId()));
+    }
+
+    private int endpointPriority(String endpointType) {
+        if (endpointType == null) {
+            return 2;
+        }
+        return switch (endpointType.toUpperCase()) {
+            case "TCP_VPC" -> 0;
+            case "TCP_INTERNET" -> 1;
+            default -> 2;
+        };
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.isBlank();
     }
 
     public InstanceVO updateInstance(InstanceVO instance) {
@@ -84,14 +180,17 @@ public class InstanceService {
         }
 
         InstanceVO updated = copyOf(existing);
+        boolean cloudInstance = existing.getVendor() != null && 
existing.getVendor() != InstanceVendor.APACHE;
         if (instance.getName() != null) {
             updated.setName(instance.getName());
         }
-        if (instance.getType() != null) {
-            updated.setType(instance.getType());
-        }
-        if (instance.getEndpoint() != null) {
-            updated.setEndpoint(instance.getEndpoint());
+        if (!cloudInstance) {
+            if (instance.getType() != null) {
+                updated.setType(instance.getType());
+            }
+            if (instance.getEndpoint() != null) {
+                updated.setEndpoint(instance.getEndpoint());
+            }
         }
         if (instance.getRemark() != null) {
             updated.setRemark(instance.getRemark());
@@ -125,6 +224,10 @@ public class InstanceService {
                 .remark(instance.getRemark())
                 .type(instance.getType())
                 .endpoint(instance.getEndpoint())
+                .vendor(instance.getVendor() == null ? InstanceVendor.APACHE : 
instance.getVendor())
+                .cloudInstanceId(instance.getCloudInstanceId())
+                .credentialId(instance.getCredentialId())
+                .regionId(instance.getRegionId())
                 .topicCount(instance.getTopicCount())
                 .consumerGroupCount(instance.getConsumerGroupCount())
                 .build();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceVO.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceVO.java
index 6332ba80..bc86d347 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceVO.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.instance;
 
 import org.apache.rocketmq.studio.common.domain.BaseEntity;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -35,6 +36,10 @@ public class InstanceVO extends BaseEntity {
     private String remark;
     private InstanceType type;
     private String endpoint;
+    private InstanceVendor vendor;
+    private String cloudInstanceId;
+    private String credentialId;
+    private String regionId;
     private int topicCount;
     private int consumerGroupCount;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
index e2f600e7..e903af4f 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/MybatisPlusInstanceRepository.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.instance;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.persistence.entity.RmqGroup;
 import org.apache.rocketmq.studio.persistence.entity.RmqInstance;
 import org.apache.rocketmq.studio.persistence.entity.RmqTopic;
@@ -114,6 +115,15 @@ public class MybatisPlusInstanceRepository implements 
InstanceRepository {
         instanceMapper.deleteById(id);
     }
 
+    @Override
+    public boolean existsByCredentialId(String credentialId) {
+        if (credentialId == null || credentialId.isBlank()) {
+            return false;
+        }
+        return instanceMapper.selectCount(
+                new QueryWrapper<RmqInstance>().eq("credential_id", 
credentialId)) > 0;
+    }
+
     private List<InstanceVO> withCounts(List<RmqInstance> entities) {
         if (entities.isEmpty()) {
             return List.of();
@@ -149,6 +159,10 @@ public class MybatisPlusInstanceRepository implements 
InstanceRepository {
                 .remark(entity.getRemark())
                 .type(parseType(entity.getType()))
                 .endpoint(entity.getEndpoint())
+                .vendor(parseVendor(entity.getVendor()))
+                .cloudInstanceId(entity.getCloudInstanceId())
+                .credentialId(entity.getCredentialId())
+                .regionId(entity.getRegionId())
                 .topicCount(topicCounts.getOrDefault(entity.getId(), 
0L).intValue())
                 .consumerGroupCount(groupCounts.getOrDefault(entity.getId(), 
0L).intValue())
                 .build();
@@ -166,6 +180,14 @@ public class MybatisPlusInstanceRepository implements 
InstanceRepository {
         }
     }
 
+    private InstanceVendor parseVendor(String vendor) {
+        try {
+            return InstanceVendor.valueOf(vendor);
+        } catch (IllegalArgumentException | NullPointerException ex) {
+            return InstanceVendor.APACHE;
+        }
+    }
+
     private RmqInstance toEntity(InstanceVO vo) {
         RmqInstance entity = new RmqInstance();
         entity.setId(vo.getId());
@@ -173,6 +195,10 @@ public class MybatisPlusInstanceRepository implements 
InstanceRepository {
         entity.setRemark(vo.getRemark());
         entity.setType(vo.getType() == null ? null : vo.getType().name());
         entity.setEndpoint(vo.getEndpoint());
+        entity.setVendor(vo.getVendor() == null ? InstanceVendor.APACHE.name() 
: vo.getVendor().name());
+        entity.setCloudInstanceId(vo.getCloudInstanceId());
+        entity.setCredentialId(vo.getCredentialId());
+        entity.setRegionId(vo.getRegionId());
         entity.setCreatedAt(vo.getCreatedAt());
         entity.setUpdatedAt(vo.getUpdatedAt());
         return entity;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
index a5a6baaf..674ac107 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
@@ -16,7 +16,9 @@
  */
 package org.apache.rocketmq.studio.instance.dlq;
 
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
@@ -31,19 +33,30 @@ import java.util.List;
 public class DLQService {
 
     private final DLQProvider dlqProvider;
+    private final InstanceProviderRegistry providerRegistry;
 
     public List<DLQGroupVO> listDLQGroups(String instanceId) {
+        requireApacheInstance(instanceId);
         log.info("Listing DLQ groups for instance: {}", instanceId);
         return dlqProvider.listDLQGroups(instanceId);
     }
 
     public DLQResendResultVO resendMessages(String instanceId, String 
groupName, Long startTime, Long endTime,
                                              String targetTopic) {
+        requireApacheInstance(instanceId);
         validateResendRequest(groupName, startTime, endTime);
         log.info("Resending DLQ messages: group={}, targetTopic={}", 
groupName, targetTopic);
         return dlqProvider.resendMessages(instanceId, groupName, startTime, 
endTime, targetTopic);
     }
 
+    private void requireApacheInstance(String instanceId) {
+        providerRegistry.byInstanceId(instanceId).ifPresent(provider -> {
+            if (provider.vendor() != InstanceVendor.APACHE) {
+                throw new BusinessException(501, "DLQ operations are not 
supported for cloud instances");
+            }
+        });
+    }
+
     private void validateResendRequest(String groupName, Long startTime, Long 
endTime) {
         if (!StringUtils.hasText(groupName)) {
             throw new BusinessException(HttpStatus.BAD_REQUEST.value(), 
"groupName is required");
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
index a9a2715b..4d7ddf70 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
@@ -41,24 +41,31 @@ public class ConsumerGroupController {
 
     @GetMapping
     public Result<List<ConsumerGroupVO>> listConsumerGroups(
+            @RequestParam(required = false) String instanceId,
             @RequestParam(required = false) String clusterId,
             @RequestParam(required = false) String search) {
-        return Result.ok(metadataService.listConsumerGroups(clusterId, 
search));
+        return Result.ok(metadataService.listConsumerGroups(instanceId, 
clusterId, search));
     }
 
     @GetMapping("/{name}")
-    public Result<ConsumerGroupVO> getConsumerGroup(@PathVariable String name) 
{
-        return Result.ok(metadataService.getConsumerGroup(name));
+    public Result<ConsumerGroupVO> getConsumerGroup(
+            @PathVariable String name,
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(metadataService.getConsumerGroup(instanceId, name));
     }
 
     @GetMapping("/{name}/progress")
-    public Result<List<QueueProgressVO>> getGroupProgress(@PathVariable String 
name) {
-        return Result.ok(metadataService.getGroupProgress(name));
+    public Result<List<QueueProgressVO>> getGroupProgress(
+            @PathVariable String name,
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(metadataService.getGroupProgress(instanceId, name));
     }
 
     @GetMapping("/{name}/subscriptions")
-    public Result<List<SubscriptionEntryVO>> 
getGroupSubscriptions(@PathVariable String name) {
-        return Result.ok(metadataService.getGroupSubscriptions(name));
+    public Result<List<SubscriptionEntryVO>> getGroupSubscriptions(
+            @PathVariable String name,
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(metadataService.getGroupSubscriptions(instanceId, 
name));
     }
 
     @GetMapping("/{name}/instances/{clientId}/stack")
@@ -75,13 +82,14 @@ public class ConsumerGroupController {
 
     @PostMapping("/delete")
     public Result<Void> deleteConsumerGroup(@Valid @RequestBody 
DeleteConsumerGroupDTO request) {
-        metadataService.deleteConsumerGroup(request.getName());
+        metadataService.deleteConsumerGroup(request.getInstanceId(), 
request.getName());
         return Result.ok();
     }
 
     @PostMapping("/reset-offset")
     public Result<Void> resetOffset(@Valid @RequestBody ResetConsumerOffsetDTO 
request) {
-        metadataService.resetOffset(request.getName(), request.getTimestamp(), 
request.getTopic());
+        metadataService.resetOffset(request.getInstanceId(), request.getName(),
+                request.getTimestamp(), request.getTopic());
         return Result.ok();
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/CreateConsumerGroupDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/CreateConsumerGroupDTO.java
index 8175a889..5ebde988 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/CreateConsumerGroupDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/CreateConsumerGroupDTO.java
@@ -37,8 +37,11 @@ public class CreateConsumerGroupDTO {
     @PositiveOrZero(message = "delaySeconds must be zero or positive")
     private Integer delaySeconds;
 
+    private String instanceId;
+
     public ConsumerGroupVO toConsumerGroupVO() {
         ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setInstanceId(instanceId);
         group.setName(name);
         group.setNamespace(namespace);
         group.setClusterId(clusterId);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/DeleteConsumerGroupDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/DeleteConsumerGroupDTO.java
index 69cc2ec2..5a3e5caa 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/DeleteConsumerGroupDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/DeleteConsumerGroupDTO.java
@@ -29,4 +29,6 @@ import lombok.NoArgsConstructor;
 public class DeleteConsumerGroupDTO {
     @NotBlank(message = "name is required")
     private String name;
+
+    private String instanceId;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
index b7f5265c..64111393 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
@@ -37,4 +37,6 @@ public class ResetConsumerOffsetDTO {
     private Long timestamp;
 
     private String topic;
+
+    private String instanceId;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
index 58412c06..21d0a4e5 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.instance.message;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -31,17 +32,22 @@ public class MessageService {
     private static final long MAX_TOPIC_QUERY_WINDOW_MILLIS = 7L * 24 * 60 * 
60 * 1000;
 
     private final MessageProvider messageProvider;
+    private final InstanceProviderRegistry providerRegistry;
 
     public List<MessageRecordVO> queryMessages(
             String instanceId, String topic, String msgId, String tag, String 
key, Long startTime, Long endTime) {
         validateTopicQueryWindow(topic, msgId, key, startTime, endTime);
         log.info("Querying messages: topic={}, msgId={}, tag={}, key={}", 
topic, msgId, tag, key);
-        return messageProvider.queryMessages(instanceId, topic, msgId, tag, 
key, startTime, endTime);
+        return providerRegistry.byInstanceId(instanceId)
+                .map(provider -> provider.queryMessages(instanceId, topic, 
msgId, tag, key, startTime, endTime))
+                .orElseGet(() -> messageProvider.queryMessages(instanceId, 
topic, msgId, tag, key, startTime, endTime));
     }
 
     public TraceRecordVO getMessageTrace(String instanceId, String msgId) {
         log.info("Getting message trace: msgId={}", msgId);
-        return messageProvider.getMessageTrace(instanceId, msgId);
+        return providerRegistry.byInstanceId(instanceId)
+                .map(provider -> provider.getMessageTrace(instanceId, msgId))
+                .orElseGet(() -> messageProvider.getMessageTrace(instanceId, 
msgId));
     }
 
     private void validateTopicQueryWindow(String topic, String msgId, String 
key, Long startTime, Long endTime) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/CreateTopicDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/CreateTopicDTO.java
index 2f0146d5..efae80d6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/CreateTopicDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/CreateTopicDTO.java
@@ -36,8 +36,11 @@ public class CreateTopicDTO {
     private TopicPerm perm;
     private String remark;
 
+    private String instanceId;
+
     public TopicVO toTopicVO() {
         TopicVO topic = new TopicVO();
+        topic.setInstanceId(instanceId);
         topic.setName(name);
         topic.setNamespace(namespace);
         topic.setClusterId(clusterId);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
index 50f256c7..746e887c 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
@@ -29,4 +29,6 @@ import lombok.NoArgsConstructor;
 public class DeleteTopicDTO {
     @NotBlank(message = "name is required")
     private String name;
+
+    private String instanceId;
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
index 4a829d5e..94093daf 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
@@ -16,16 +16,20 @@
  */
 package org.apache.rocketmq.studio.instance.topic;
 
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
 import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
 import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.provider.InstanceProvider;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
+import java.util.Optional;
 
 @Slf4j
 @Service
@@ -34,47 +38,87 @@ public class MetadataService {
 
     private final MetadataProvider metadataProvider;
     private final AdminClient adminClient;
+    private final InstanceProviderRegistry providerRegistry;
 
     // ── TopicVO ───────────────────────────────────────────────────────
 
 
     public List<TopicVO> listTopics(String clusterId, String type, String 
search) {
-        return metadataProvider.listTopics(
-                normalizeFilter(clusterId),
-                normalizeFilter(type),
-                normalizeFilter(search));
+        return listTopics(null, clusterId, type, search);
+    }
+
+    public List<TopicVO> listTopics(String instanceId, String clusterId, 
String type, String search) {
+        return providerFor(instanceId)
+                .map(provider -> provider.listTopics(instanceId, 
normalizeFilter(type), normalizeFilter(search)))
+                .orElseGet(() -> metadataProvider.listTopics(
+                        normalizeFilter(clusterId),
+                        normalizeFilter(type),
+                        normalizeFilter(search)));
     }
 
 
     public TopicVO createTopic(TopicVO topic) {
         requireTopic(topic);
-        return adminClient.createTopic(topic);
+        return providerFor(topic.getInstanceId())
+                .map(provider -> provider.createTopic(topic.getInstanceId(), 
topic))
+                .orElseGet(() -> adminClient.createTopic(topic));
     }
 
 
     public TopicVO updateTopic(TopicVO topic) {
         requireTopic(topic);
-        return adminClient.updateTopic(topic);
+        return providerFor(topic.getInstanceId())
+                .map(provider -> provider.updateTopic(topic.getInstanceId(), 
topic))
+                .orElseGet(() -> adminClient.updateTopic(topic));
     }
 
 
     public void deleteTopic(String name) {
-        adminClient.deleteTopic(name);
+        deleteTopic(null, name);
+    }
+
+    public void deleteTopic(String instanceId, String name) {
+        Optional<InstanceProvider> provider = providerFor(instanceId);
+        if (provider.isPresent()) {
+            provider.get().deleteTopic(instanceId, name);
+        } else {
+            adminClient.deleteTopic(name);
+        }
     }
 
 
     public List<BrokerRouteVO> getTopicRoutes(String name) {
+        return getTopicRoutes(null, name);
+    }
+
+    public List<BrokerRouteVO> getTopicRoutes(String instanceId, String name) {
+        Optional<InstanceProvider> provider = providerFor(instanceId);
+        if (provider.isPresent() && provider.get().vendor() != 
InstanceVendor.APACHE) {
+            // broker routing does not apply to serverless cloud instances
+            return List.of();
+        }
         return metadataProvider.getTopicRoutes(name);
     }
 
 
     public List<TopicConsumerVO> getTopicConsumers(String name) {
-        return metadataProvider.getTopicConsumers(name);
+        return getTopicConsumers(null, name);
+    }
+
+    public List<TopicConsumerVO> getTopicConsumers(String instanceId, String 
name) {
+        return providerFor(instanceId)
+                .map(provider -> provider.getTopicConsumers(instanceId, name))
+                .orElseGet(() -> metadataProvider.getTopicConsumers(name));
     }
 
 
     public SendMessageVO sendMessage(SendMessageDTO request) {
         requireSendMessageRequest(request);
+        providerFor(request.getInstanceId()).ifPresent(provider -> {
+            if (provider.vendor() != InstanceVendor.APACHE) {
+                throw new BusinessException(501, "Sending messages is not 
supported for cloud instances");
+            }
+        });
         return adminClient.sendMessage(request);
     }
 
@@ -82,37 +126,85 @@ public class MetadataService {
 
 
     public List<ConsumerGroupVO> listConsumerGroups(String clusterId, String 
search) {
-        return metadataProvider.listConsumerGroups(normalizeFilter(clusterId), 
normalizeFilter(search));
+        return listConsumerGroups(null, clusterId, search);
+    }
+
+    public List<ConsumerGroupVO> listConsumerGroups(String instanceId, String 
clusterId, String search) {
+        return providerFor(instanceId)
+                .map(provider -> provider.listConsumerGroups(instanceId, 
normalizeFilter(search)))
+                .orElseGet(() -> metadataProvider.listConsumerGroups(
+                        normalizeFilter(clusterId), normalizeFilter(search)));
     }
 
 
     public ConsumerGroupVO getConsumerGroup(String name) {
+        return getConsumerGroup(null, name);
+    }
+
+    public ConsumerGroupVO getConsumerGroup(String instanceId, String name) {
+        providerFor(instanceId).ifPresent(provider -> {
+            if (provider.vendor() != InstanceVendor.APACHE) {
+                throw new BusinessException(501, "Consumer group detail is not 
supported for cloud instances");
+            }
+        });
         return adminClient.getConsumerGroup(name);
     }
 
 
     public List<QueueProgressVO> getGroupProgress(String name) {
-        return metadataProvider.getGroupProgress(name);
+        return getGroupProgress(null, name);
+    }
+
+    public List<QueueProgressVO> getGroupProgress(String instanceId, String 
name) {
+        return providerFor(instanceId)
+                .map(provider -> provider.getGroupProgress(instanceId, name))
+                .orElseGet(() -> metadataProvider.getGroupProgress(name));
     }
 
 
     public List<SubscriptionEntryVO> getGroupSubscriptions(String name) {
-        return metadataProvider.getGroupSubscriptions(name);
+        return getGroupSubscriptions(null, name);
+    }
+
+    public List<SubscriptionEntryVO> getGroupSubscriptions(String instanceId, 
String name) {
+        return providerFor(instanceId)
+                .map(provider -> provider.getGroupSubscriptions(instanceId, 
name))
+                .orElseGet(() -> metadataProvider.getGroupSubscriptions(name));
     }
 
 
     public ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group) {
-        return adminClient.createConsumerGroup(group);
+        return providerFor(group == null ? null : group.getInstanceId())
+                .map(provider -> 
provider.createConsumerGroup(group.getInstanceId(), group))
+                .orElseGet(() -> adminClient.createConsumerGroup(group));
     }
 
 
     public void deleteConsumerGroup(String name) {
-        adminClient.deleteConsumerGroup(name);
+        deleteConsumerGroup(null, name);
+    }
+
+    public void deleteConsumerGroup(String instanceId, String name) {
+        Optional<InstanceProvider> provider = providerFor(instanceId);
+        if (provider.isPresent()) {
+            provider.get().deleteConsumerGroup(instanceId, name);
+        } else {
+            adminClient.deleteConsumerGroup(name);
+        }
     }
 
 
     public void resetOffset(String name, long timestamp, String topic) {
-        adminClient.resetOffset(name, timestamp, topic);
+        resetOffset(null, name, timestamp, topic);
+    }
+
+    public void resetOffset(String instanceId, String name, long timestamp, 
String topic) {
+        Optional<InstanceProvider> provider = providerFor(instanceId);
+        if (provider.isPresent()) {
+            provider.get().resetOffset(instanceId, name, timestamp, topic);
+        } else {
+            adminClient.resetOffset(name, timestamp, topic);
+        }
     }
 
     // ── NamespaceVO ───────────────────────────────────────────────────
@@ -122,6 +214,10 @@ public class MetadataService {
         throw new BusinessException(501, "Namespace discovery is not 
implemented by the current metadata provider");
     }
 
+    private Optional<InstanceProvider> providerFor(String instanceId) {
+        return providerRegistry.byInstanceId(instanceId);
+    }
+
     private String normalizeFilter(String value) {
         return value == null || value.isBlank() ? null : value.trim();
     }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/SendMessageDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/SendMessageDTO.java
index 375a72fe..21f343a6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/SendMessageDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/SendMessageDTO.java
@@ -29,6 +29,8 @@ import java.util.Map;
 @NoArgsConstructor
 @AllArgsConstructor
 public class SendMessageDTO {
+    private String instanceId;
+
     @NotBlank(message = "topic is required")
     private String topic;
     private String tag;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
index e1309334..6caeda88 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
@@ -39,10 +39,11 @@ public class TopicController {
 
     @GetMapping
     public Result<List<TopicVO>> listTopics(
+            @RequestParam(required = false) String instanceId,
             @RequestParam(required = false) String clusterId,
             @RequestParam(required = false) String type,
             @RequestParam(required = false) String search) {
-        return Result.ok(metadataService.listTopics(clusterId, type, search));
+        return Result.ok(metadataService.listTopics(instanceId, clusterId, 
type, search));
     }
 
     @PostMapping("/create")
@@ -60,18 +61,22 @@ public class TopicController {
     @PostMapping("/delete")
     public Result<Void> deleteTopic(@Valid @RequestBody(required = false) 
DeleteTopicDTO request) {
         requireDeleteTopicRequest(request);
-        metadataService.deleteTopic(request.getName());
+        metadataService.deleteTopic(request.getInstanceId(), 
request.getName());
         return Result.ok();
     }
 
     @GetMapping("/{name}/routes")
-    public Result<List<BrokerRouteVO>> getTopicRoutes(@PathVariable String 
name) {
-        return Result.ok(metadataService.getTopicRoutes(name));
+    public Result<List<BrokerRouteVO>> getTopicRoutes(
+            @PathVariable String name,
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(metadataService.getTopicRoutes(instanceId, name));
     }
 
     @GetMapping("/{name}/consumers")
-    public Result<List<TopicConsumerVO>> getTopicConsumers(@PathVariable 
String name) {
-        return Result.ok(metadataService.getTopicConsumers(name));
+    public Result<List<TopicConsumerVO>> getTopicConsumers(
+            @PathVariable String name,
+            @RequestParam(required = false) String instanceId) {
+        return Result.ok(metadataService.getTopicConsumers(instanceId, name));
     }
 
     @PostMapping("/send")
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqCloudCredential.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqCloudCredential.java
new file mode 100644
index 00000000..155e79f5
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqCloudCredential.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.persistence.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.time.LocalDateTime;
+
+@TableName("rmq_cloud_credential")
+public class RmqCloudCredential {
+
+    @TableId(type = IdType.INPUT)
+    private String id;
+
+    private String name;
+
+    private String vendor;
+
+    private String accessKey;
+
+    private String secretKey;
+
+    private String remark;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getVendor() {
+        return vendor;
+    }
+
+    public void setVendor(String vendor) {
+        this.vendor = vendor;
+    }
+
+    public String getAccessKey() {
+        return accessKey;
+    }
+
+    public void setAccessKey(String accessKey) {
+        this.accessKey = accessKey;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public LocalDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(LocalDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public LocalDateTime getUpdatedAt() {
+        return updatedAt;
+    }
+
+    public void setUpdatedAt(LocalDateTime updatedAt) {
+        this.updatedAt = updatedAt;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
index 7c4cdbfd..c172e113 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
@@ -38,6 +38,14 @@ public class RmqInstance {
 
     private String endpoint;
 
+    private String vendor;
+
+    private String cloudInstanceId;
+
+    private String credentialId;
+
+    private String regionId;
+
     private LocalDateTime createdAt;
 
     private LocalDateTime updatedAt;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqCloudCredentialMapper.java
similarity index 69%
copy from 
server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqCloudCredentialMapper.java
index 50f256c7..07571149 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/persistence/mapper/RmqCloudCredentialMapper.java
@@ -14,19 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.instance.topic;
+package org.apache.rocketmq.studio.persistence.mapper;
 
-import jakarta.validation.constraints.NotBlank;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.rocketmq.studio.persistence.entity.RmqCloudCredential;
 
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class DeleteTopicDTO {
-    @NotBlank(message = "name is required")
-    private String name;
+public interface RmqCloudCredentialMapper extends 
BaseMapper<RmqCloudCredential> {
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudCatalogProvider.java
similarity index 56%
copy from 
server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/provider/CloudCatalogProvider.java
index 53410e6f..eb9584cb 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudCatalogProvider.java
@@ -14,26 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.rocketmq.studio.provider;
 
-package org.apache.rocketmq.studio.instance;
-
-import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 
 import java.util.List;
-import java.util.Optional;
-
-public interface InstanceRepository {
-    List<InstanceVO> findAll();
 
-    List<InstanceVO> findByType(InstanceType type);
-
-    List<InstanceVO> search(String keyword);
+/**
+ * Cloud catalog discovery SPI: list regions / cloud instances with a stored 
credential.
+ * Commercial instances are never created manually; users pick one from this 
catalog.
+ */
+public interface CloudCatalogProvider {
 
-    List<InstanceVO> findByTypeAndSearch(InstanceType type, String keyword);
+    InstanceVendor vendor();
 
-    Optional<InstanceVO> findById(String id);
+    List<CloudRegionVO> listRegions(String credentialId);
 
-    InstanceVO save(InstanceVO instance);
+    List<CloudInstanceOptionVO> listCloudInstances(String credentialId, String 
regionId, String search);
 
-    void deleteById(String id);
+    CloudInstanceDetailVO getCloudInstance(String credentialId, String 
regionId, String cloudInstanceId);
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/CloudInstanceDetailVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudInstanceDetailVO.java
new file mode 100644
index 00000000..ef736b36
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudInstanceDetailVO.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider;
+
+import java.util.List;
+
+public class CloudInstanceDetailVO {
+
+    private String instanceId;
+    private String instanceName;
+    private String status;
+    private String regionId;
+    private String remark;
+    private List<CloudEndpoint> endpoints;
+
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    public void setInstanceId(String instanceId) {
+        this.instanceId = instanceId;
+    }
+
+    public String getInstanceName() {
+        return instanceName;
+    }
+
+    public void setInstanceName(String instanceName) {
+        this.instanceName = instanceName;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public String getRegionId() {
+        return regionId;
+    }
+
+    public void setRegionId(String regionId) {
+        this.regionId = regionId;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public List<CloudEndpoint> getEndpoints() {
+        return endpoints;
+    }
+
+    public void setEndpoints(List<CloudEndpoint> endpoints) {
+        this.endpoints = endpoints;
+    }
+
+    public static class CloudEndpoint {
+        private String endpointType;
+        private String endpointUrl;
+
+        public CloudEndpoint() {
+        }
+
+        public CloudEndpoint(String endpointType, String endpointUrl) {
+            this.endpointType = endpointType;
+            this.endpointUrl = endpointUrl;
+        }
+
+        public String getEndpointType() {
+            return endpointType;
+        }
+
+        public void setEndpointType(String endpointType) {
+            this.endpointType = endpointType;
+        }
+
+        public String getEndpointUrl() {
+            return endpointUrl;
+        }
+
+        public void setEndpointUrl(String endpointUrl) {
+            this.endpointUrl = endpointUrl;
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/CloudInstanceOptionVO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudInstanceOptionVO.java
new file mode 100644
index 00000000..e3d1da28
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudInstanceOptionVO.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider;
+
+public class CloudInstanceOptionVO {
+
+    private String instanceId;
+    private String instanceName;
+    private String status;
+    private String regionId;
+    private Integer topicCount;
+    private Integer groupCount;
+    private String remark;
+
+    public String getInstanceId() {
+        return instanceId;
+    }
+
+    public void setInstanceId(String instanceId) {
+        this.instanceId = instanceId;
+    }
+
+    public String getInstanceName() {
+        return instanceName;
+    }
+
+    public void setInstanceName(String instanceName) {
+        this.instanceName = instanceName;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public String getRegionId() {
+        return regionId;
+    }
+
+    public void setRegionId(String regionId) {
+        this.regionId = regionId;
+    }
+
+    public Integer getTopicCount() {
+        return topicCount;
+    }
+
+    public void setTopicCount(Integer topicCount) {
+        this.topicCount = topicCount;
+    }
+
+    public Integer getGroupCount() {
+        return groupCount;
+    }
+
+    public void setGroupCount(Integer groupCount) {
+        this.groupCount = groupCount;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
 b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudRegionVO.java
similarity index 56%
copy from 
server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
copy to 
server/src/main/java/org/apache/rocketmq/studio/provider/CloudRegionVO.java
index 7c4cdbfd..2a068a4b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/persistence/entity/RmqInstance.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/CloudRegionVO.java
@@ -14,31 +14,34 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.persistence.entity;
+package org.apache.rocketmq.studio.provider;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
+public class CloudRegionVO {
 
-import java.time.LocalDateTime;
+    private String regionId;
+    private String regionName;
 
-@Data
-@TableName("rmq_instance")
-public class RmqInstance {
+    public CloudRegionVO() {
+    }
 
-    @TableId(type = IdType.INPUT)
-    private String id;
+    public CloudRegionVO(String regionId, String regionName) {
+        this.regionId = regionId;
+        this.regionName = regionName;
+    }
 
-    private String name;
+    public String getRegionId() {
+        return regionId;
+    }
 
-    private String remark;
+    public void setRegionId(String regionId) {
+        this.regionId = regionId;
+    }
 
-    private String type;
+    public String getRegionName() {
+        return regionName;
+    }
 
-    private String endpoint;
-
-    private LocalDateTime createdAt;
-
-    private LocalDateTime updatedAt;
+    public void setRegionName(String regionName) {
+        this.regionName = regionName;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
new file mode 100644
index 00000000..65a9a945
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
+import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+import org.apache.rocketmq.studio.instance.message.TraceRecordVO;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+import java.util.List;
+
+/**
+ * Unified instance-scoped operations SPI. Every method takes the Studio 
instance id as its
+ * first argument; implementations resolve the target cluster / cloud instance 
themselves.
+ * Unsupported operations throw {@link UnsupportedOperationException} (mapped 
to HTTP 501).
+ */
+public interface InstanceProvider {
+
+    InstanceVendor vendor();
+
+    List<TopicVO> listTopics(String instanceId, String type, String search);
+
+    TopicVO createTopic(String instanceId, TopicVO topic);
+
+    TopicVO updateTopic(String instanceId, TopicVO topic);
+
+    void deleteTopic(String instanceId, String topicName);
+
+    List<TopicConsumerVO> getTopicConsumers(String instanceId, String 
topicName);
+
+    List<ConsumerGroupVO> listConsumerGroups(String instanceId, String search);
+
+    ConsumerGroupVO createConsumerGroup(String instanceId, ConsumerGroupVO 
group);
+
+    void deleteConsumerGroup(String instanceId, String groupName);
+
+    List<QueueProgressVO> getGroupProgress(String instanceId, String 
groupName);
+
+    List<SubscriptionEntryVO> getGroupSubscriptions(String instanceId, String 
groupName);
+
+    void resetOffset(String instanceId, String groupName, long timestamp, 
String topic);
+
+    List<MessageRecordVO> queryMessages(String instanceId, String topic, 
String msgId,
+                                        String tag, String key, Long 
startTime, Long endTime);
+
+    TraceRecordVO getMessageTrace(String instanceId, String msgId);
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProviderRegistry.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProviderRegistry.java
new file mode 100644
index 00000000..998a81a6
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProviderRegistry.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.springframework.stereotype.Component;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Component
+public class InstanceProviderRegistry {
+
+    private final Map<InstanceVendor, InstanceProvider> providers = new 
EnumMap<>(InstanceVendor.class);
+    private final Map<InstanceVendor, CloudCatalogProvider> catalogs = new 
EnumMap<>(InstanceVendor.class);
+    private final InstanceRepository instanceRepository;
+
+    public InstanceProviderRegistry(List<InstanceProvider> providerList,
+                                    List<CloudCatalogProvider> catalogList,
+                                    InstanceRepository instanceRepository) {
+        providerList.forEach(provider -> providers.put(provider.vendor(), 
provider));
+        catalogList.forEach(catalog -> catalogs.put(catalog.vendor(), 
catalog));
+        this.instanceRepository = instanceRepository;
+    }
+
+    public InstanceProvider forVendor(InstanceVendor vendor) {
+        InstanceProvider provider = providers.get(vendor);
+        if (provider == null) {
+            throw new BusinessException(501, "No instance provider registered 
for vendor " + vendor);
+        }
+        return provider;
+    }
+
+    /**
+     * Resolves the provider for the given Studio instance id. Returns empty 
for a blank id
+     * so callers can fall back to the legacy global behavior.
+     */
+    public Optional<InstanceProvider> byInstanceId(String instanceId) {
+        if (instanceId == null || instanceId.isBlank()) {
+            return Optional.empty();
+        }
+        InstanceVO instance = instanceRepository.findById(instanceId)
+                .orElseThrow(() -> new BusinessException(404, "Instance not 
found: " + instanceId));
+        InstanceVendor vendor = instance.getVendor() == null ? 
InstanceVendor.APACHE : instance.getVendor();
+        return Optional.of(forVendor(vendor));
+    }
+
+    public CloudCatalogProvider catalogFor(InstanceVendor vendor) {
+        CloudCatalogProvider catalog = catalogs.get(vendor);
+        if (catalog == null) {
+            throw new BusinessException(501, "No cloud catalog provider 
registered for vendor " + vendor);
+        }
+        return catalog;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
new file mode 100644
index 00000000..1d351546
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/ApacheInstanceProvider.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider.apache;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
+import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.instance.message.MessageProvider;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+import org.apache.rocketmq.studio.instance.message.TraceRecordVO;
+import org.apache.rocketmq.studio.instance.topic.AdminClient;
+import org.apache.rocketmq.studio.instance.topic.MetadataProvider;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+import org.apache.rocketmq.studio.provider.InstanceProvider;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Open-source Apache RocketMQ implementation: pure delegation to the existing 
admin-client
+ * beans, keeping behavior identical to the pre-provider code path.
+ */
+@Component
+public class ApacheInstanceProvider implements InstanceProvider {
+
+    private final MetadataProvider metadataProvider;
+    private final AdminClient adminClient;
+    private final MessageProvider messageProvider;
+
+    public ApacheInstanceProvider(MetadataProvider metadataProvider,
+                                  AdminClient adminClient,
+                                  MessageProvider messageProvider) {
+        this.metadataProvider = metadataProvider;
+        this.adminClient = adminClient;
+        this.messageProvider = messageProvider;
+    }
+
+    @Override
+    public InstanceVendor vendor() {
+        return InstanceVendor.APACHE;
+    }
+
+    @Override
+    public List<TopicVO> listTopics(String instanceId, String type, String 
search) {
+        return metadataProvider.listTopics(null, type, search).stream()
+                .filter(topic -> matchesInstance(topic.getInstanceId(), 
instanceId))
+                .toList();
+    }
+
+    @Override
+    public TopicVO createTopic(String instanceId, TopicVO topic) {
+        return adminClient.createTopic(topic);
+    }
+
+    @Override
+    public TopicVO updateTopic(String instanceId, TopicVO topic) {
+        return adminClient.updateTopic(topic);
+    }
+
+    @Override
+    public void deleteTopic(String instanceId, String topicName) {
+        adminClient.deleteTopic(topicName);
+    }
+
+    @Override
+    public List<TopicConsumerVO> getTopicConsumers(String instanceId, String 
topicName) {
+        return metadataProvider.getTopicConsumers(topicName);
+    }
+
+    @Override
+    public List<ConsumerGroupVO> listConsumerGroups(String instanceId, String 
search) {
+        return metadataProvider.listConsumerGroups(null, search).stream()
+                .filter(group -> matchesInstance(group.getInstanceId(), 
instanceId))
+                .toList();
+    }
+
+    @Override
+    public ConsumerGroupVO createConsumerGroup(String instanceId, 
ConsumerGroupVO group) {
+        return adminClient.createConsumerGroup(group);
+    }
+
+    @Override
+    public void deleteConsumerGroup(String instanceId, String groupName) {
+        adminClient.deleteConsumerGroup(groupName);
+    }
+
+    @Override
+    public List<QueueProgressVO> getGroupProgress(String instanceId, String 
groupName) {
+        return metadataProvider.getGroupProgress(groupName);
+    }
+
+    @Override
+    public List<SubscriptionEntryVO> getGroupSubscriptions(String instanceId, 
String groupName) {
+        return metadataProvider.getGroupSubscriptions(groupName);
+    }
+
+    @Override
+    public void resetOffset(String instanceId, String groupName, long 
timestamp, String topic) {
+        adminClient.resetOffset(groupName, timestamp, topic);
+    }
+
+    @Override
+    public List<MessageRecordVO> queryMessages(String instanceId, String 
topic, String msgId,
+                                               String tag, String key, Long 
startTime, Long endTime) {
+        return messageProvider.queryMessages(instanceId, topic, msgId, tag, 
key, startTime, endTime);
+    }
+
+    @Override
+    public TraceRecordVO getMessageTrace(String instanceId, String msgId) {
+        return messageProvider.getMessageTrace(instanceId, msgId);
+    }
+
+    private boolean matchesInstance(String topicInstanceId, String instanceId) 
{
+        if (instanceId == null || instanceId.isBlank()) {
+            return true;
+        }
+        return Objects.equals(topicInstanceId, instanceId);
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
new file mode 100644
index 00000000..ca4df5f4
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider.tencent;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
+import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+import org.apache.rocketmq.studio.instance.message.TraceRecordVO;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+import org.apache.rocketmq.studio.provider.InstanceProvider;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Tencent Cloud TDMQ placeholder: package structure reserved, all operations 
unsupported.
+ */
+@Component
+public class TencentInstanceProvider implements InstanceProvider {
+
+    private static final String NOT_IMPLEMENTED = "Tencent Cloud provider is 
not implemented yet";
+
+    @Override
+    public InstanceVendor vendor() {
+        return InstanceVendor.TENCENT;
+    }
+
+    @Override
+    public List<TopicVO> listTopics(String instanceId, String type, String 
search) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public TopicVO createTopic(String instanceId, TopicVO topic) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public TopicVO updateTopic(String instanceId, TopicVO topic) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public void deleteTopic(String instanceId, String topicName) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public List<TopicConsumerVO> getTopicConsumers(String instanceId, String 
topicName) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public List<ConsumerGroupVO> listConsumerGroups(String instanceId, String 
search) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public ConsumerGroupVO createConsumerGroup(String instanceId, 
ConsumerGroupVO group) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public void deleteConsumerGroup(String instanceId, String groupName) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public List<QueueProgressVO> getGroupProgress(String instanceId, String 
groupName) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public List<SubscriptionEntryVO> getGroupSubscriptions(String instanceId, 
String groupName) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public void resetOffset(String instanceId, String groupName, long 
timestamp, String topic) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public List<MessageRecordVO> queryMessages(String instanceId, String 
topic, String msgId,
+                                               String tag, String key, Long 
startTime, Long endTime) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+
+    @Override
+    public TraceRecordVO getMessageTrace(String instanceId, String msgId) {
+        throw new UnsupportedOperationException(NOT_IMPLEMENTED);
+    }
+}
diff --git a/server/src/main/resources/db/schema.sql 
b/server/src/main/resources/db/schema.sql
index 8475f8cd..53aed259 100644
--- a/server/src/main/resources/db/schema.sql
+++ b/server/src/main/resources/db/schema.sql
@@ -25,6 +25,10 @@ CREATE TABLE IF NOT EXISTS rmq_instance (
   remark VARCHAR(255),
   type VARCHAR(32) NOT NULL COMMENT 'PROXY/DIRECT',
   endpoint VARCHAR(512) NOT NULL,
+  vendor VARCHAR(32) NOT NULL DEFAULT 'APACHE' COMMENT 'APACHE/ALIYUN/TENCENT',
+  cloud_instance_id VARCHAR(128) COMMENT '云厂商实例 ID(vendor 非 APACHE 时必填)',
+  credential_id VARCHAR(64) COMMENT '引用 rmq_cloud_credential.id',
+  region_id VARCHAR(64) COMMENT '云 region',
   created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
   updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -209,6 +213,19 @@ CREATE TABLE IF NOT EXISTS rmq_system_alert (
   INDEX idx_acknowledged (acknowledged)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
+-- 15. 云厂商凭据(secret_key 为 base64 编码,禁止明文;access_key 明文用于唯一键与打码展示)
+CREATE TABLE IF NOT EXISTS rmq_cloud_credential (
+  id VARCHAR(64) PRIMARY KEY,
+  name VARCHAR(128) NOT NULL COMMENT '凭据显示名',
+  vendor VARCHAR(32) NOT NULL COMMENT 'ALIYUN/TENCENT',
+  access_key VARCHAR(255) NOT NULL,
+  secret_key VARCHAR(512) NOT NULL COMMENT 'base64 编码的 SK',
+  remark VARCHAR(255),
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_vendor_access_key (vendor, access_key)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
 -- ============================================================
 -- 样例数据(幂等):instance / topic / group 列表以本库为准,创建时写库、读取时读库。
 -- 实例管理页默认 5 个实例:2 个 DIRECT(instance-direct-1/2)+ 3 个 
PROXY(instance-proxy-1/2/3)。
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialServiceTest.java
new file mode 100644
index 00000000..5bb98e59
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cloud/credential/CloudCredentialServiceTest.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.cloud.credential;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class CloudCredentialServiceTest {
+
+    @Mock
+    private CloudCredentialRepository credentialRepository;
+
+    @Mock
+    private InstanceRepository instanceRepository;
+
+    @InjectMocks
+    private CloudCredentialService service;
+
+    @Test
+    void listShouldMaskAccessKeyAndHideSecretTest() {
+        CloudCredentialVO stored = new CloudCredentialVO();
+        stored.setId("cred-1");
+        stored.setName("aliyun-test");
+        stored.setVendor(InstanceVendor.ALIYUN);
+        stored.setAccessKey("LTAI5tUnitTestKey000000001");
+        stored.setSecretKey("secret-value");
+        when(credentialRepository.findAll()).thenReturn(List.of(stored));
+
+        List<CloudCredentialVO> result = service.listMasked();
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getAccessKey()).isEqualTo("LTAI****0001");
+        assertThat(result.get(0).getSecretKey()).isNull();
+    }
+
+    @Test
+    void listShouldFullyMaskShortAccessKeyTest() {
+        CloudCredentialVO stored = new CloudCredentialVO();
+        stored.setVendor(InstanceVendor.ALIYUN);
+        stored.setAccessKey("short-ak");
+        when(credentialRepository.findAll()).thenReturn(List.of(stored));
+
+        
assertThat(service.listMasked().get(0).getAccessKey()).isEqualTo("****");
+    }
+
+    @Test
+    void createShouldRejectDuplicateVendorAndAccessKeyTest() {
+        CloudCredentialVO request = new CloudCredentialVO();
+        request.setName("dup");
+        request.setVendor(InstanceVendor.ALIYUN);
+        request.setAccessKey("LTAI5tDupKey000000000001");
+        request.setSecretKey("sk");
+        
when(credentialRepository.findByVendorAndAccessKey(InstanceVendor.ALIYUN, 
"LTAI5tDupKey000000000001"))
+                .thenReturn(Optional.of(new CloudCredentialVO()));
+
+        assertThatThrownBy(() -> service.create(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("already exists");
+        verify(credentialRepository, never()).save(any());
+    }
+
+    @Test
+    void createShouldRejectApacheVendorTest() {
+        CloudCredentialVO request = new CloudCredentialVO();
+        request.setName("bad");
+        request.setVendor(InstanceVendor.APACHE);
+        request.setAccessKey("LTAI5tBadVendor000000001");
+        request.setSecretKey("sk");
+
+        assertThatThrownBy(() -> service.create(request))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("ALIYUN or TENCENT");
+    }
+
+    @Test
+    void createShouldAssignIdAndMaskResultTest() {
+        CloudCredentialVO request = new CloudCredentialVO();
+        request.setName("ok");
+        request.setVendor(InstanceVendor.ALIYUN);
+        request.setAccessKey("LTAI5tGoodKey00000000001");
+        request.setSecretKey("sk-value");
+        when(credentialRepository.findByVendorAndAccessKey(any(), 
any())).thenReturn(Optional.empty());
+        when(credentialRepository.save(any(CloudCredentialVO.class)))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+
+        CloudCredentialVO created = service.create(request);
+
+        assertThat(created.getId()).isNotBlank();
+        assertThat(created.getAccessKey()).isEqualTo("LTAI****0001");
+        assertThat(created.getSecretKey()).isNull();
+    }
+
+    @Test
+    void deleteShouldRejectWhenReferencedByInstanceTest() {
+        CloudCredentialVO stored = new CloudCredentialVO();
+        stored.setId("cred-1");
+        
when(credentialRepository.findById("cred-1")).thenReturn(Optional.of(stored));
+        
when(instanceRepository.existsByCredentialId("cred-1")).thenReturn(true);
+
+        assertThatThrownBy(() -> service.delete("cred-1"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("referenced");
+        verify(credentialRepository, never()).deleteById(any());
+    }
+
+    @Test
+    void revealShouldReturnUnmaskedCredentialTest() {
+        CloudCredentialVO stored = new CloudCredentialVO();
+        stored.setId("cred-1");
+        stored.setVendor(InstanceVendor.ALIYUN);
+        stored.setAccessKey("LTAI5tRevealKey000000001");
+        stored.setSecretKey("plain-secret");
+        
when(credentialRepository.findById("cred-1")).thenReturn(Optional.of(stored));
+
+        CloudCredentialVO revealed = service.reveal("cred-1");
+
+        
assertThat(revealed.getAccessKey()).isEqualTo("LTAI5tRevealKey000000001");
+        assertThat(revealed.getSecretKey()).isEqualTo("plain-secret");
+    }
+
+    @Test
+    void repositoryShouldBase64EncodeSecretTest() {
+        String encoded = 
MybatisPlusCloudCredentialRepository.encodeBase64("plain-secret");
+        assertThat(encoded).isNotEqualTo("plain-secret");
+        
assertThat(MybatisPlusCloudCredentialRepository.decodeBase64(encoded)).isEqualTo("plain-secret");
+    }
+
+    @Test
+    void repositoryShouldTolerateLegacyPlainSecretTest() {
+        assertThat(MybatisPlusCloudCredentialRepository.decodeBase64("not 
base64 !!!")).isEqualTo("not base64 !!!");
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
index 64cbc05d..096339c9 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
@@ -17,8 +17,14 @@
 
 package org.apache.rocketmq.studio.instance;
 
+import org.apache.rocketmq.studio.cloud.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.cloud.credential.CloudCredentialVO;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.provider.CloudCatalogProvider;
+import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
@@ -43,6 +49,12 @@ class InstanceServiceTest {
     @Mock
     private InstanceRepository instanceRepository;
 
+    @Mock
+    private CloudCredentialRepository cloudCredentialRepository;
+
+    @Mock
+    private InstanceProviderRegistry providerRegistry;
+
     @InjectMocks
     private InstanceService instanceService;
 
@@ -418,4 +430,100 @@ class InstanceServiceTest {
                 .isInstanceOf(BusinessException.class)
                 .hasMessage("InstanceVO not found: missing");
     }
+
+    @Test
+    void createInstanceShouldDefaultToApacheVendorTest() {
+        InstanceVO instance = 
InstanceVO.builder().name("inst").endpoint("10.0.0.1:8080").build();
+        
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        InstanceVO created = instanceService.createInstance(instance);
+
+        assertThat(created.getVendor()).isEqualTo(InstanceVendor.APACHE);
+        verifyNoInteractions(cloudCredentialRepository, providerRegistry);
+    }
+
+    @Test
+    void createInstanceShouldRejectManualEndpointForAliyunTest() {
+        InstanceVO instance = InstanceVO.builder()
+                .vendor(InstanceVendor.ALIYUN)
+                .endpoint("rmq-xxx.cn-hangzhou.rmq.aliyuncs.com:8080")
+                .build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(instance))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("cannot be set manually");
+        verify(instanceRepository, never()).save(any(InstanceVO.class));
+    }
+
+    @Test
+    void createInstanceShouldRequireCloudFieldsForAliyunTest() {
+        InstanceVO instance = 
InstanceVO.builder().vendor(InstanceVendor.ALIYUN).build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(instance))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("credentialId");
+    }
+
+    @Test
+    void createInstanceShouldResolveAliyunEndpointFromCatalogTest() {
+        InstanceVO instance = InstanceVO.builder()
+                .vendor(InstanceVendor.ALIYUN)
+                .credentialId("cred-1")
+                .cloudInstanceId("rmq-cn-xxx")
+                .regionId("cn-hangzhou")
+                .build();
+        CloudCredentialVO credential = new CloudCredentialVO();
+        credential.setId("cred-1");
+        credential.setVendor(InstanceVendor.ALIYUN);
+        
when(cloudCredentialRepository.findById("cred-1")).thenReturn(Optional.of(credential));
+        CloudCatalogProvider catalog = 
org.mockito.Mockito.mock(CloudCatalogProvider.class);
+        CloudInstanceDetailVO detail = new CloudInstanceDetailVO();
+        detail.setInstanceId("rmq-cn-xxx");
+        detail.setInstanceName("prod-mq");
+        detail.setEndpoints(List.of(
+                new CloudInstanceDetailVO.CloudEndpoint("TCP_INTERNET", 
"public:8080"),
+                new CloudInstanceDetailVO.CloudEndpoint("TCP_VPC", 
"vpc:8080")));
+        
when(providerRegistry.catalogFor(InstanceVendor.ALIYUN)).thenReturn(catalog);
+        when(catalog.getCloudInstance("cred-1", "cn-hangzhou", 
"rmq-cn-xxx")).thenReturn(detail);
+        
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        InstanceVO created = instanceService.createInstance(instance);
+
+        assertThat(created.getName()).isEqualTo("prod-mq");
+        assertThat(created.getEndpoint()).isEqualTo("vpc:8080");
+        assertThat(created.getType()).isEqualTo(InstanceType.PROXY);
+    }
+
+    @Test
+    void createInstanceShouldRejectTencentVendorTest() {
+        InstanceVO instance = 
InstanceVO.builder().vendor(InstanceVendor.TENCENT).build();
+
+        assertThatThrownBy(() -> instanceService.createInstance(instance))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(501));
+    }
+
+    @Test
+    void updateInstanceShouldKeepCloudFieldsImmutableTest() {
+        InstanceVO existing = InstanceVO.builder()
+                .name("aliyun-inst")
+                .vendor(InstanceVendor.ALIYUN)
+                .cloudInstanceId("rmq-cn-xxx")
+                .credentialId("cred-1")
+                .regionId("cn-hangzhou")
+                .type(InstanceType.PROXY)
+                .endpoint("vpc:8080")
+                .build();
+        existing.setId("inst-1");
+        
when(instanceRepository.findById("inst-1")).thenReturn(Optional.of(existing));
+        
when(instanceRepository.save(any(InstanceVO.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+
+        InstanceVO request = 
InstanceVO.builder().endpoint("hacked:8080").remark("updated").build();
+        request.setId("inst-1");
+        InstanceVO updated = instanceService.updateInstance(request);
+
+        assertThat(updated.getEndpoint()).isEqualTo("vpc:8080");
+        assertThat(updated.getRemark()).isEqualTo("updated");
+        assertThat(updated.getCloudInstanceId()).isEqualTo("rmq-cn-xxx");
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
index 2cb65468..8f33cefc 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.rocketmq.studio.instance.dlq;
 
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
@@ -37,6 +38,9 @@ class DLQServiceTest {
     @Mock
     private DLQProvider dlqProvider;
 
+    @Mock
+    private InstanceProviderRegistry providerRegistry;
+
     @InjectMocks
     private DLQService dlqService;
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
index 82ad24da..09b9f718 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
@@ -35,6 +35,7 @@ import java.util.Map;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -169,7 +170,7 @@ class ConsumerGroupControllerTest {
                 .andExpect(jsonPath("$.code").value(200))
                 .andExpect(jsonPath("$.message").value("success"));
 
-        verify(metadataService).resetOffset(eq("cg-orders"), 
eq(1784246400000L), eq("orders"));
+        verify(metadataService).resetOffset(isNull(), eq("cg-orders"), 
eq(1784246400000L), eq("orders"));
     }
 
     @Test
@@ -181,7 +182,7 @@ class ConsumerGroupControllerTest {
                 .andExpect(jsonPath("$.code").value(200))
                 .andExpect(jsonPath("$.message").value("success"));
 
-        verify(metadataService).deleteConsumerGroup("cg-orders");
+        verify(metadataService).deleteConsumerGroup(isNull(), eq("cg-orders"));
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
index 2c8de5a3..52128c8f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageServiceTest.java
@@ -11,6 +11,7 @@
 package org.apache.rocketmq.studio.instance.message;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -22,7 +23,8 @@ class MessageServiceTest {
     @Test
     void rejectsReversedTopicQueryWindowBeforeCallingProvider() {
         MessageProvider provider = mock(MessageProvider.class);
-        MessageService service = new MessageService(provider);
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        MessageService service = new MessageService(provider, registry);
 
         assertThatThrownBy(() -> service.queryMessages("instance-a", "TopicA", 
null, null, null, 200L, 100L))
                 .isInstanceOf(BusinessException.class)
@@ -34,7 +36,8 @@ class MessageServiceTest {
     @Test
     void rejectsTopicQueryWindowLongerThanSevenDaysBeforeCallingProvider() {
         MessageProvider provider = mock(MessageProvider.class);
-        MessageService service = new MessageService(provider);
+        InstanceProviderRegistry registry = 
mock(InstanceProviderRegistry.class);
+        MessageService service = new MessageService(provider, registry);
 
         assertThatThrownBy(() -> service.queryMessages("instance-a", "TopicA", 
null, null, null, 0L,
                 8L * 24 * 60 * 60 * 1000))
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java
index a7c0d7d3..7ed2013b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/MetadataServiceTest.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.instance.topic;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
@@ -43,6 +44,9 @@ class MetadataServiceTest {
     @Mock
     private AdminClient adminClient;
 
+    @Mock
+    private InstanceProviderRegistry providerRegistry;
+
     @InjectMocks
     private MetadataService metadataService;
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
index 06753721..6da4af95 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
@@ -62,7 +62,7 @@ class TopicControllerTest {
         topic.setWriteQueues(8);
         topic.setReadQueues(8);
 
-        when(metadataService.listTopics(isNull(), isNull(), 
isNull())).thenReturn(List.of(topic));
+        when(metadataService.listTopics(isNull(), isNull(), isNull(), 
isNull())).thenReturn(List.of(topic));
 
         mockMvc.perform(get("/api/topics"))
                 .andExpect(status().isOk())
@@ -74,7 +74,7 @@ class TopicControllerTest {
 
     @Test
     void listTopicsShouldPassQueryParams() throws Exception {
-        when(metadataService.listTopics(eq("cluster-1"), eq("NORMAL"), 
eq("test")))
+        when(metadataService.listTopics(isNull(), eq("cluster-1"), 
eq("NORMAL"), eq("test")))
                 .thenReturn(List.of());
 
         mockMvc.perform(get("/api/topics")
@@ -84,7 +84,7 @@ class TopicControllerTest {
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data").isArray());
 
-        verify(metadataService).listTopics(eq("cluster-1"), eq("NORMAL"), 
eq("test"));
+        verify(metadataService).listTopics(isNull(), eq("cluster-1"), 
eq("NORMAL"), eq("test"));
     }
 
     @Test
@@ -230,7 +230,7 @@ class TopicControllerTest {
                 .andExpect(jsonPath("$.code").value(200))
                 .andExpect(jsonPath("$.message").value("success"));
 
-        verify(metadataService).deleteTopic("test-topic");
+        verify(metadataService).deleteTopic(isNull(), eq("test-topic"));
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderRegistryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderRegistryTest.java
new file mode 100644
index 00000000..633c5f3e
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderRegistryTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class InstanceProviderRegistryTest {
+
+    @Mock
+    private InstanceRepository instanceRepository;
+
+    private InstanceProviderRegistry registry;
+    private InstanceProvider apacheProvider;
+    private InstanceProvider aliyunProvider;
+
+    @BeforeEach
+    void setUp() {
+        apacheProvider = stubProvider(InstanceVendor.APACHE);
+        aliyunProvider = stubProvider(InstanceVendor.ALIYUN);
+        registry = new InstanceProviderRegistry(List.of(apacheProvider, 
aliyunProvider), List.of(), instanceRepository);
+    }
+
+    @Test
+    void forVendorShouldReturnRegisteredProviderTest() {
+        
assertThat(registry.forVendor(InstanceVendor.ALIYUN)).isSameAs(aliyunProvider);
+    }
+
+    @Test
+    void forVendorShouldThrowWhenMissingTest() {
+        assertThatThrownBy(() -> registry.forVendor(InstanceVendor.TENCENT))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(501));
+    }
+
+    @Test
+    void byInstanceIdShouldReturnEmptyForBlankIdTest() {
+        assertThat(registry.byInstanceId(null)).isEmpty();
+        assertThat(registry.byInstanceId("  ")).isEmpty();
+    }
+
+    @Test
+    void byInstanceIdShouldThrowWhenInstanceMissingTest() {
+        
when(instanceRepository.findById("missing")).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> registry.byInstanceId("missing"))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(404));
+    }
+
+    @Test
+    void byInstanceIdShouldResolveVendorProviderTest() {
+        InstanceVO instance = 
InstanceVO.builder().vendor(InstanceVendor.ALIYUN).build();
+        
when(instanceRepository.findById("inst-1")).thenReturn(Optional.of(instance));
+
+        
assertThat(registry.byInstanceId("inst-1")).containsSame(aliyunProvider);
+    }
+
+    @Test
+    void byInstanceIdShouldDefaultToApacheWhenVendorNullTest() {
+        InstanceVO instance = InstanceVO.builder().build();
+        
when(instanceRepository.findById("inst-2")).thenReturn(Optional.of(instance));
+
+        
assertThat(registry.byInstanceId("inst-2")).containsSame(apacheProvider);
+    }
+
+    @Test
+    void catalogForShouldThrowWhenNoCatalogRegisteredTest() {
+        assertThatThrownBy(() -> registry.catalogFor(InstanceVendor.ALIYUN))
+                .isInstanceOf(BusinessException.class)
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(501));
+    }
+
+    private InstanceProvider stubProvider(InstanceVendor vendor) {
+        InstanceProvider provider = mock(InstanceProvider.class);
+        when(provider.vendor()).thenReturn(vendor);
+        return provider;
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
new file mode 100644
index 00000000..b7660bac
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.rocketmq.studio.provider.tencent;
+
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class TencentInstanceProviderTest {
+
+    private final TencentInstanceProvider provider = new 
TencentInstanceProvider();
+
+    @Test
+    void vendorShouldBeTencentTest() {
+        assertThat(provider.vendor()).isEqualTo(InstanceVendor.TENCENT);
+    }
+
+    @Test
+    void allOperationsShouldThrowUnsupportedTest() {
+        assertThatThrownBy(() -> provider.listTopics("inst", null, null))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.createTopic("inst", new TopicVO()))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.updateTopic("inst", new TopicVO()))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.deleteTopic("inst", "topic"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.getTopicConsumers("inst", "topic"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.listConsumerGroups("inst", null))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.createConsumerGroup("inst", new 
ConsumerGroupVO()))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.deleteConsumerGroup("inst", "group"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.getGroupProgress("inst", "group"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.getGroupSubscriptions("inst", 
"group"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.resetOffset("inst", "group", 1L, 
"topic"))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.queryMessages("inst", "topic", null, 
null, null, null, null))
+                .isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> provider.getMessageTrace("inst", "msg"))
+                .isInstanceOf(UnsupportedOperationException.class);
+    }
+}

Reply via email to