How Multi-Instance Storage Backends Are Bound Per Workspace and Knowledge Base in WeKnora
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, 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 (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 constructs these identifiers using the storage:// scheme:
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 parses the storage scheme:
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 and retrieves the full configuration from the storage_backends table defined in 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:
- Configuration Inheritance: Retrieve the knowledge base's
StorageBackendID; if unset, use the workspace'sDefaultStorageBackendIDfrominternal/types/tenant.go. - URI Construction: Call
StorageBackendPathto generate a URI embedding the selected backend ID and the provider-specific path. - Request Processing: When accessing the file, call
unwrapStorageBackendPathto extract the backend ID and provider path. - Backend Validation: Query the
storage_backendstable viainternal/types/storagebackend.goand verify againstinternal/storageallowlist/allowlist.go. - Provider Execution: Instantiate the provider-specific client (local, MinIO, COS) and execute the file operation.
The sandbox implementation in 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:
// 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 (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
DefaultStorageBackendIDininternal/types/tenant.go, while knowledge bases override viaStorageBackendIDininternal/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 againstinternal/storageallowlist/allowlist.go, and retrieves full backend configurations frominternal/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. When processing requests, the system unwraps the URI and validates the extracted backend ID against the allow-list in 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 (lines 92-95). When this field is non-null, the system uses it instead of the workspace's DefaultStorageBackendID from 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, 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 (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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →