How WeKnora Handles Multi-Instance Storage Backends: Architecture and Implementation
WeKnora handles multi-instance storage backends by storing each storage configuration as a distinct StorageBackend object with a UUID, binding knowledge bases to specific instances via the StorageBackendID field, and resolving the correct backend at runtime using the storage:// URI wrapper parsed by the FileServiceResolver.
Tencent's WeKnora is a knowledge-engineering platform that decouples storage configuration from knowledge base definitions. Unlike systems limited to a single storage engine per deployment, WeKnora's multi-instance storage backend architecture allows a single workspace to manage multiple independent object stores—such as MinIO, Amazon S3, and Tencent COS—simultaneously, with per-knowledge-base granularity. According to the Tencent/WeKnora source code, this flexibility is achieved through a combination of database-backed storage definitions, tenant-level defaults, and runtime URI resolution.
The StorageBackend Data Model
WeKnora's storage layer treats each backend as an independent database entity. The StorageBackend struct defined in [internal/types/storagebackend.go](https://github.com/Tencent/WeKnora/blob/main/internal/types/storagebackend.go) contains the fields necessary to isolate instances:
- ID: A UUID generated automatically on
BeforeCreatethat uniquely identifies the backend. - TenantID: The workspace (tenant) that owns this backend.
- Provider: The underlying implementation (
local,minio,s3,cos). - Config: Provider-specific connection details including endpoints, credentials, and bucket names.
- Source: Indicates creation method (
"user"for UI-created,"env"for process-wide defaults). - Status: Soft-deletion control (
"active"or"disabled").
Additionally, the Tenant struct in [internal/types/tenant.go](https://github.com/Tencent/WeKnora/blob/main/internal/types/tenant.go) declares DefaultStorageBackendID. This field establishes which backend to use when a knowledge base does not specify its own, creating a fallback chain from explicit binding to tenant default to environment variable.
Binding Knowledge Bases to Storage Instances
The binding mechanism centers on an optional field in the KnowledgeBase struct located in [internal/types/knowledgebase.go](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go):
StorageBackendID *string `json:"storage_backend_id,omitempty"`
When StorageBackendID is set, the knowledge base is explicitly bound to that specific storage instance. If the field is nil, WeKnora falls back to the tenant's DefaultStorageBackendID, and ultimately to the process-wide default defined by the STORAGE_TYPE environment variable.
The helper method SharesStorageBackendWith in the same file compares two knowledge bases for backend compatibility:
func (kb *KnowledgeBase) SharesStorageBackendWith(other *KnowledgeBase, defaultBackendID, defaultProvider string) bool
This method first checks for explicit StorageBackendID matches on both sides. Only when neither knowledge base has an explicit ID does it compare the effective storage provider, ensuring accurate conflict detection during import and export operations.
Runtime Resolution with the storage:// URI
To address a specific backend instance at runtime, WeKnora uses a URI wrapper scheme. File references stored in the database use standard provider schemes (minio://, s3://), but to target a specific backend instance, the system wraps them as follows:
storage://<backend-id>/<provider>://<path>
Utility functions in [internal/types/knowledgebase.go](https://github.com/Tencent/WeKnora/blob/main/internal/types/knowledgebase.go) handle this conversion:
func BuildStorageBackendPath(backendID, providerPath string) string {
return "storage://" + strings.TrimSpace(backendID) + "/" + providerPath
}
func ParseStorageBackendPath(path string) (backendID, providerPath string, ok bool)
When the server needs to retrieve a file, the FileServiceResolver in [internal/storageurl/resolver.go](https://github.com/Tencent/WeKnora/blob/main/internal/storageurl/resolver.go) performs the following steps:
- Detect
resource://references and route to the tenant-wide default. - Parse the
storage://wrapper usingParseStorageBackendPathto extract thebackendID. - Determine the provider by inspecting the inner URI scheme via
ParseProviderScheme, falling back toStorageEngineConfig.DefaultProviderif absent. - Cache the resolved client per request using a map keyed by
backendID:providerto avoid SDK client recreation. - Resolve the concrete backend by loading the
StorageBackendrow from the database and constructing aFileServiceviafilesvc.NewFileServiceFromStorageConfig. - Fallback to a local file service if resolution fails or the provider is
local.
Implementing Multi-Instance Storage in Practice
Creating a Storage Backend Programmatically
To create a new backend instance in code, use the StorageBackend type and repository pattern:
package main
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
func createS3Backend(ctx context.Context, repo interfaces.StorageBackendRepository) error {
backend := &types.StorageBackend{
TenantID: 12345,
Name: "S3 Production",
Provider: "s3",
Config: types.StorageBackendConfig{
Endpoint: "https://s3.amazonaws.com",
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
BucketName: "prod-knowledge-base",
UseSSL: true,
},
}
if err := backend.Validate(); err != nil {
return err
}
return repo.Create(ctx, backend)
}
The Validate() method enforces provider-specific constraints, while BeforeCreate automatically generates the UUID that serves as the backendID for URI construction.
Generating Storage-Wrapped File References
When storing file metadata that must reference a specific backend, use the utility functions to build the wrapper:
import "github.com/Tencent/WeKnora/internal/types"
func makeFileReference(backendID, bucketPath string) string {
// bucketPath format: "provider://bucket/key"
return types.BuildStorageBackendPath(backendID, bucketPath)
}
// Example usage:
ref := makeFileReference("c3f5e1d2-9a4b-4d1a-a5f3-e7b9c2d5f6a7", "minio://documents/report.pdf")
// Result: storage://c3f5e1d2-9a4b-4d1a-a5f3-e7b9c2d5f6a7/minio://documents/report.pdf
Resolving File Services at Runtime
To stream or presign URLs for files stored across different backends, instantiate the resolver:
import (
"github.com/Tencent/WeKnora/internal/storageurl"
"github.com/Tencent/WeKnora/internal/types"
)
func getFileService(tenant *types.Tenant, storagePath string) (interfaces.FileService, error) {
resolver := storageurl.NewFileServiceResolver(tenant, file.DefaultFileService)
return resolver.ResolveFileService(storagePath)
}
// Usage:
svc, err := getFileService(tenant, "storage://abc123/cos://bucket/image.png")
if err != nil {
// handle resolution failure
}
url, err := svc.Presign("cos://bucket/image.png") // Returns presigned URL
The resolver handles credential retrieval, client initialization, and caching automatically based on the backendID embedded in the path.
Security and Legacy Compatibility
Multi-instance storage requires careful credential management. In [internal/types/storagebackend.go](https://github.com/Tencent/WeKnora/blob/main/internal/types/storagebackend.go), the StorageBackendConfig encrypts sensitive fields like SecretAccessKey using AES encryption via utils.GetAESKey. When the API returns a backend configuration to clients, the NewStorageBackendResponse function masks secrets with a redacted placeholder to prevent credential leakage.
For existing deployments using the legacy single-storage configuration, WeKnora provides migration helpers. The StorageBackendFromLegacy function converts a StorageEngineConfig into a concrete StorageBackend row, while StorageBackendFromEnvironment creates backend definitions derived from environment variables, ensuring backward compatibility during upgrades.
Summary
- Database-backed instances: Each storage backend is a distinct row with a UUID, enabling unlimited instances per tenant.
- Explicit binding: Knowledge bases link to backends via
StorageBackendID, with automatic fallback to tenant defaults. - URI-based resolution: The
storage://<id>/<provider>://wrapper allows per-file backend selection parsed byFileServiceResolver. - Runtime efficiency: Per-request caching of SDK clients prevents connection overhead when accessing multiple backends.
- Secure by default: Credentials are encrypted at rest and redacted in API responses.
Frequently Asked Questions
Can a single knowledge base use multiple storage backends simultaneously?
No, a knowledge base binds to exactly one storage backend via its StorageBackendID field. However, different knowledge bases within the same workspace can each bind to different backends, and file references within a knowledge base can technically point to other backends via explicit storage:// URIs, though this bypasses the knowledge base's default binding.
How does WeKnora handle storage backend credentials securely?
According to the source code in internal/types/storagebackend.go, secrets within StorageBackendConfig are encrypted using AES with a key retrieved from utils.GetAESKey. When the backend configuration is serialized for API responses, the NewStorageBackendResponse utility replaces sensitive values with a RedactedSecretPlaceholder, ensuring credentials never travel over the wire in plaintext.
What happens if a knowledge base doesn't specify a storage backend?
If StorageBackendID is nil, WeKnora checks the tenant's DefaultStorageBackendID field. If that is also unset, the system falls back to the process-wide default derived from the STORAGE_TYPE environment variable. This three-tier fallback ensures that legacy configurations continue to function while allowing gradual adoption of explicit multi-instance bindings.
Is it possible to migrate from a legacy single-storage setup to multi-instance?
Yes, WeKnora includes migration utilities in internal/types/storagebackend.go. The StorageBackendFromLegacy function converts a legacy StorageEngineConfig into a concrete StorageBackend database row, preserving existing connections. Additionally, StorageBackendFromEnvironment creates backend instances from environment variables for deployments that prefer infrastructure-as-code over UI management.
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 →