# How Multi-Instance Storage Backends Are Bound Per Workspace and Knowledge Base in WeKnora

> Discover how WeKnora binds multi-instance storage backends per workspace and knowledge base. Learn about hierarchical storage binding and tenant isolation.

- Repository: [Tencent/WeKnora](https://github.com/tencent/WeKnora)
- Tags: internals
- Published: 2026-09-12

---

**WeKnora implements hierarchical storage binding by allowing workspaces to configure default backends while permitting individual knowledge bases to override them via scoped storage URIs that embed the specific backend ID, ensuring complete tenant isolation.**

In Tencent's WeKnora knowledge management platform, multi-tenant data isolation requires precise control over where each workspace and knowledge base stores its files. The system binds **multi-instance storage backends per workspace and knowledge base** through a reference-based URI scheme that explicitly encodes the storage backend identifier at runtime.

## Hierarchical Storage Backend Configuration

### Workspace-Level Default Backends

The tenant (workspace) structure defines the fallback storage location for all contained knowledge bases. In [`internal/types/tenant.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/tenant.go), the `Tenant` struct includes a `DefaultStorageBackendID` field (lines 112-115) that specifies which concrete storage instance to use when a knowledge base does not declare its own.

When a workspace administrator configures `default_storage_backend_id` in the tenant configuration, every knowledge base within that tenant inherits this backend unless explicitly overridden. This provides a convenient default while reducing configuration overhead for standard deployments.

### Knowledge Base Overrides

Individual knowledge bases can specify dedicated storage backends through the `StorageBackendID` field defined in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go) (lines 92-95). This field overrides the workspace default, allowing specific knowledge bases to target alternative storage instances such as separate MinIO clusters or cloud object storage buckets.

The binding logic checks the knowledge base's `StorageBackendID` first. If null, it falls back to the workspace's `DefaultStorageBackendID`, creating a clean inheritance chain that maintains flexibility without sacrificing isolation.

## Storage URI Construction and Resolution

### Building Scoped Storage URIs

WeKnora uses a custom URI scheme to embed backend references directly into file paths. The `StorageBackendPath` function in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go) constructs these identifiers using the `storage://` scheme:

```go
const storageBackendScheme = "storage://"

func StorageBackendPath(backendID, providerPath string) string {
    // Example: "storage://backend-a/local://1/abc/img.png"
    return storageBackendScheme + strings.TrimSpace(backendID) + "/" + providerPath
}

```

This approach encodes the backend selection directly into the URI, making the storage target explicit and auditable. The `backendID` parameter comes from either the knowledge base's specific configuration or the workspace default, while `providerPath` represents the provider-specific location (e.g., `local://1/abc/img.png`).

### URI Unwrapping and Backend Resolution

When processing file operations, the system unwraps these URIs to locate the concrete backend. The `unwrapStorageBackendPath` function in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go) parses the storage scheme:

```go
func unwrapStorageBackendPath(filePath string) (backendID, providerPath string, err error) {
    if !strings.HasPrefix(filePath, storageBackendScheme) {
        return "", "", fmt.Errorf("not a storage:// URI")
    }
    rest := strings.TrimPrefix(filePath, storageBackendScheme)
    parts := strings.SplitN(rest, "/", 2)
    if len(parts) != 2 {
        return "", "", fmt.Errorf("malformed storage URI")
    }
    return parts[0], parts[1], nil
}

```

After extracting the `backendID`, the system validates the backend against the allow-list in [`internal/storageallowlist/allowlist.go`](https://github.com/Tencent/WeKnora/blob/main/internal/storageallowlist/allowlist.go) and retrieves the full configuration from the `storage_backends` table defined in [`internal/types/storagebackend.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/storagebackend.go) (lines 26-45). This configuration includes the provider type (local filesystem, MinIO, COS), endpoint URLs, and credentials.

## Runtime Binding Flow

### Resolution Process

The complete binding flow follows these steps:

1. **Configuration Inheritance**: Retrieve the knowledge base's `StorageBackendID`; if unset, use the workspace's `DefaultStorageBackendID` from [`internal/types/tenant.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/tenant.go).
2. **URI Construction**: Call `StorageBackendPath` to generate a URI embedding the selected backend ID and the provider-specific path.
3. **Request Processing**: When accessing the file, call `unwrapStorageBackendPath` to extract the backend ID and provider path.
4. **Backend Validation**: Query the `storage_backends` table via [`internal/types/storagebackend.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/storagebackend.go) and verify against [`internal/storageallowlist/allowlist.go`](https://github.com/Tencent/WeKnora/blob/main/internal/storageallowlist/allowlist.go).
5. **Provider Execution**: Instantiate the provider-specific client (local, MinIO, COS) and execute the file operation.

The sandbox implementation in [`internal/sandbox/session_manager.go`](https://github.com/Tencent/WeKnora/blob/main/internal/sandbox/session_manager.go) (lines 58-80) leverages these storage URIs within workspace-scoped sessions, ensuring that temporary file operations respect the same backend binding rules as persistent storage.

### Implementation Example

The following code demonstrates the complete binding logic from configuration to file write:

```go
// Resolve effective backend ID
kb := getKBByID(ctx, kbID)
tenant := getTenantByID(ctx, kb.TenantID)

backendID := kb.StorageBackendID
if backendID == nil {
    backendID = tenant.DefaultStorageBackendID
}

// Construct storage URI
providerPath := fmt.Sprintf("local://%d/%s", tenant.ID, "uploads/image.png")
storageURI := StorageBackendPath(*backendID, providerPath)
// Result: "storage://backend-42/local://10008/uploads/image.png"

// Resolve and use backend
bID, provPath, err := unwrapStorageBackendPath(storageURI)
if err != nil {
    log.Fatal(err)
}

backend, err := storageBackendRepo.GetByID(ctx, bID)
if err != nil {
    log.Fatal(err)
}

client := backend.NewProviderClient()
err = client.PutObject(ctx, provPath, fileReader, fileSize, nil)

```

Additional validation occurs in [`internal/utils/presign.go`](https://github.com/Tencent/WeKnora/blob/main/internal/utils/presign.go) (lines 140-165), where the system verifies that the unwrapped backend ID matches the caller's tenant and knowledge base scope before generating presigned URLs.

## Summary

- **WeKnora binds storage backends hierarchically**: Workspaces configure defaults via `DefaultStorageBackendID` in [`internal/types/tenant.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/tenant.go), while knowledge bases override via `StorageBackendID` in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go).
- **Storage URIs embed backend IDs**: The `storage://` scheme explicitly references the backend instance, preventing cross-tenant leakage and enabling precise access control.
- **Runtime resolution validates scope**: The system unwraps URIs using `unwrapStorageBackendPath`, validates against [`internal/storageallowlist/allowlist.go`](https://github.com/Tencent/WeKnora/blob/main/internal/storageallowlist/allowlist.go), and retrieves full backend configurations from [`internal/types/storagebackend.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/storagebackend.go).
- **Inheritance logic ensures fallback**: Knowledge bases automatically use workspace defaults when no specific backend is configured, maintaining operational simplicity.

## Frequently Asked Questions

### How does WeKnora prevent cross-tenant storage access?

WeKnora prevents cross-tenant access by embedding the backend ID directly into the storage URI via `StorageBackendPath` in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go). When processing requests, the system unwraps the URI and validates the extracted backend ID against the allow-list in [`internal/storageallowlist/allowlist.go`](https://github.com/Tencent/WeKnora/blob/main/internal/storageallowlist/allowlist.go) and the tenant-specific configuration. This ensures that even if a malicious actor crafts a URI, they cannot access backends outside their authorized scope.

### Can a knowledge base use a different backend than its workspace?

Yes. Individual knowledge bases can override the workspace default by setting their own `StorageBackendID` field, defined in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go) (lines 92-95). When this field is non-null, the system uses it instead of the workspace's `DefaultStorageBackendID` from [`internal/types/tenant.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/tenant.go), allowing specific knowledge bases to use dedicated storage instances for compliance or performance reasons.

### What happens if no storage backend is configured?

If a knowledge base does not specify a `StorageBackendID` and its parent workspace does not define a `DefaultStorageBackendID`, the system encounters a nil reference during URI construction. According to the implementation in [`internal/types/knowledgebase.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go), this results in a configuration error that must be handled by the calling code, typically resulting in a failed operation or explicit panic indicating that no storage backend is available.

### How are storage credentials managed separately from URIs?

Storage credentials and connection details are stored in the `storage_backends` table defined in [`internal/types/storagebackend.go`](https://github.com/Tencent/WeKnora/blob/main/internal/types/storagebackend.go) (lines 26-45), separate from the URI which only contains the backend ID. When `unwrapStorageBackendPath` extracts the backend ID from a storage URI, the system queries this table to retrieve the actual endpoint, credentials, and provider type (local, MinIO, COS). This separation ensures that sensitive information never appears in URIs while maintaining the ability to reference specific storage instances.