{"record":{"id":"3a48b91d5121fe20","repo":"elsa-workflows/elsa-core","slug":"the-vnext-secrets-persistence-provider-does-not-support","errorCode":null,"errorMessage":"The VNext secrets persistence provider does not support multitenancy, and the current tenant is '{tenantId}'. It stores secrets keyed by name alone, so tenants would share them. Use an Entity Framework Core secrets provider.","messagePattern":"The VNext secrets persistence provider does not support multitenancy, and the current tenant is '(.+?)'\\. It stores secrets keyed by name alone, so tenants would share them\\. Use an Entity Framework Core secrets provider\\.","errorType":"exception","errorClass":"NotSupportedException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.Secrets.Persistence.VNext/Repositories/VNextSecretRepository.cs","lineNumber":37,"sourceCode":"    /// the same document. Making it tenant-aware means changing the document id scheme, which relocates\n    /// existing documents and is a storage change to make deliberately rather than fold into a tenancy fix.\n    /// <para>\n    /// Until then this throws rather than quietly serving one tenant's secret to another. It is checked per\n    /// call rather than at startup so it catches a tenant context entered at runtime, and it stays silent for\n    /// the default tenant, which is every single-tenant deployment.\n    /// </para>\n    /// <para>\n    /// The accessor is optional because it is registered by the tenants module: a host that never added\n    /// multitenancy has none, and requiring it would break resolving this repository at all. No accessor\n    /// means no tenancy, which is the default tenant.\n    /// </para>\n    /// </remarks>\n    private void EnsureDefaultTenant()\n    {\n        var tenantId = tenantAccessor?.TenantId;\n\n        if (!string.IsNullOrEmpty(tenantId))\n            throw new NotSupportedException(\n                $\"The VNext secrets persistence provider does not support multitenancy, and the current tenant is '{tenantId}'. \"\n                + \"It stores secrets keyed by name alone, so tenants would share them. Use an Entity Framework Core secrets provider.\");\n    }\n\n    public const string StorageUnitName = \"Secrets\";\n\n    private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web)\n    {\n        Converters = { new JsonStringEnumConverter() }\n    };\n\n    public async Task<Secret?> GetAsync(string normalizedName, CancellationToken cancellationToken = default)\n    {\n        EnsureDefaultTenant();\n        var document = await documentStore.LoadAsync(StorageUnitName, NormalizeDocumentId(normalizedName), cancellationToken);\n        return document is null ? null : Deserialize(document);\n    }\n","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Secrets.Persistence.VNext/Repositories/VNextSecretRepository.cs#L19-L55","documentation":"The VNext secrets repository stores secrets keyed only by normalized name with no tenant discriminator, so it cannot isolate data between tenants. EnsureDefaultTenant is called by every read/write operation and throws NotSupportedException whenever the ambient tenant accessor reports a non-empty tenant id, preventing silent cross-tenant secret sharing.","triggerScenarios":"Any VNext repository operation (GetAsync, ListAsync, AddAsync, TryAddOrReplaceDeletedAsync, SaveAsync) executed in a multi-tenant host where the current request carries a tenant id — i.e. the app registered the VNext provider while tenancy is enabled.","commonSituations":"Enabling Elsa multitenancy after adopting the VNext secrets provider; a request scoped to a tenant (via header/token) hitting secrets APIs; environment misconfiguration where an EF Core secrets provider was intended but VNext was registered.","solutions":["Switch to the Entity Framework Core secrets provider (Elsa.Secrets.Persistence.EFCore), which supports tenant-scoped secrets, and run its migrations.","If secrets are intentionally global, run this workload in single-tenant mode so tenantAccessor.TenantId is null/empty.","Remove or bypass tenant assignment for the secrets module only if sharing secrets across tenants is acceptable — generally not recommended.","Audit configuration: ensure the DI registration matches the tenancy mode documented for the deployment."],"exampleFix":"// before\nservices.AddSecretsFeature().UseVNextPersistence(); // with multitenancy enabled\n// after\nservices.AddSecretsFeature().UseEntityFrameworkCorePersistence(); // tenant-aware store","handlingStrategy":"try-catch","validationCode":"var tenantId = tenantAccessor?.TenantId;\nif (!string.IsNullOrEmpty(tenantId) && usingVNextSecretsProvider)\n    throw new InvalidOperationException(\"Configure the EF Core secrets provider; VNext secrets do not support multitenancy.\");","typeGuard":null,"tryCatchPattern":"try\n{\n    await repository.GetAsync(name, cancellationToken);\n}\ncatch (NotSupportedException ex) when (ex.Message.Contains(\"does not support multitenancy\"))\n{\n    logger.LogCritical(ex, \"Secrets provider incompatible with multi-tenant mode; switch to EF Core provider.\");\n    throw;\n}","preventionTips":["Match the secrets persistence provider to the tenancy mode: VNext = single-tenant only.","Validate DI configuration at startup by probing with an ambient tenant set.","Document that enabling multitenancy requires the EF Core secrets provider."],"tags":["multitenancy","secrets","unsupported-operation","persistence"],"backgroundTag":"unsupported-operation","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}