elsa-workflows / elsa-workflows/elsa-core

[2.x] EF persistence permanently freezes the connection string at startup — rotated credentials are never picked up

Open
#7,765 0 comments 0 reactions 0 assignees View on GitHub
bug elsa 2 persistence triaged
Dominant language
C#
Stars
7.9k
Forks
1.5k
Avg merge
15h 22m
Merged PRs (30d)
114

Description

## Description
When using UseEntityFrameworkPersistence, the connection string is read from IConfiguration exactly once, at the first resolution of the DbContext factor, and is then frozen for the entire process lifetime. Subsequent changes to IConfiguration — including changes made by a custom configuration provider are never observed by any Elsa store.
The root cause is in UseEntityFrameworkPersistence:
```csharp
elsa.Services
.AddSingleton>() // <-- here
.AddScoped()
...
```
ElsaContextFactory constructor captures a single IDbContextFactory instance, whose DbContextOptions already contain the connection string produced by the single execution of the `configure` delegate.
Every EF store resolves contexts exclusively through that factory.
This holds for both registrations (AddPooledDbContextFactory and AddDbContextFactory) and for every value of the serviceLifetime parameter.

This matters in environments with mandatory credential rotation (e.g. banking / deployments where SQL passwords rotate every X hours with a short grace period). Once the grace period of the boot-time credential expires:
- Every Elsa DB operation fails with Login failed for user
- I noticed the issue on Quartz temporal activities firing in Cron through Elsa.Triggers table which locks the SQL account, as this fires every minute but can be seen also in StartWorkflow calls.
- Not sure if Recurring temporal workflows silently die, because the next occurrence is scheduled as part of the execution

## Steps to Reproduce
1. Create a minimal ASP.NET Core app with Elsa 2.x, EF persistence on SQL Server, Quartz temporal activities, and the attached one-activity Cron workflow (fires every minute).
2. Add a configuration source that simulates a credential rotation x seconds after startup by swapping the connection string to a broken value:
```csharp
// simulate a change in db password with a nonexisting db
builder.Configuration.Add(new DelayedConnectionStringSource("ConnectionStrings:WorkflowDb",
@"Server=(localdb)\MSSQLLocalDB;Database=NONEXISTING;Trusted_Connection=True;MultipleActiveResultSets=true"));
```
```csharp
internal class DelayedConnectionStringSource(string key, string value) : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder) =>
new DelayedConnectionStringProvider(key, value);
}

internal class DelayedConnectionStringProvider(string key, string value) : ConfigurationProvider, IDisposable
{
private Timer? _timer;

public override void Load() {
_timer = new Timer(_ => {
Data[key] = value;
OnReload();
_timer?.Dispose();
_timer = null;
}, null, TimeSpan.FromSeconds(50), Timeout.InfiniteTimeSpan);
}

public void Dispose() => _timer?.Dispose();
}
```
3. Register Elsa — the connection string is read inside the `configure` delegate:
```csharp
var configureDatabase = new Action(
(sp, ef) => ef.UseSqlServer(sp.GetRequiredService().GetConnectionString("WorkflowDb")));

builder.Services.AddDbContextFactory(configureDatabase);

builder.Services.AddElsa(elsa => {
elsa.UseEntityFrameworkPersistence(configureDatabase, autoRunMigrations: false)
.AddQuartzTemporalActivities();
// ...
});
```
4. Run the app. The Cron workflow executes once per minute against the original database/connection string.
5. After 50 seconds the provider swaps the value and raises OnReload(). Verify that IConfiguration.GetConnectionString("WorkflowDb") now returns the NONEXISTING string.
6. Elsa though, keeps executing successfully with the original database. The swapped value is never picked up. (I inverted the production failure on purpose so it runs against a single local DB: in production the old string is the one that dies, producing login failures and account lockout.)

3. **Attachments**:

Image

Minimal Cron workflow JSON below

Workflow JSON — Cron every minute → Finish
```json
{
"$id": "1",
"definitionId": "54a4a8f6ce0541018efd6383e6b26c0a",
"versionId": "178b6e9eaf2b4023b4b54101099bc514",
"name": "HelloWorld",
"displayName": "Hello World",
"version": 2,
"variables": { "$id": "2", "data": {} },
"customAttributes": { "$id": "3", "data": {} },
"isSingleton": false,
"persistenceBehavior": "WorkflowBurst",
"deleteCompletedInstances": false,
"isPublished": true,
"isLatest": true,
"tag": "HelloWorld",
"createdAt": "2026-07-11T22:18:29.7777925Z",
"activities": [
{
"$id": "4",
"activityId": "5762415e-46fa-47b3-9f64-c5cdfb737091",
"type": "Cron",
"displayName": "Cron",
"persistWorkflow": false,
"loadWorkflowContext": false,
"saveWorkflowContext": false,
"properties": [
{
"$id": "5",
"name": "CronExpression",
"expressions": { "$id": "6", "Literal": "0 0/1 * ? * * *" }
}
],
"propertyStorageProviders": { "$id": "7" }
},
{
"$id": "8",
"activityId": "a5d6a3ed-a55e-45ff-9947-2953a33e4616",
"type": "Finish",
"displayName": "Finish",
"persistWorkflow": false,
"loadWorkflowContext": false,
"saveWorkflowContext": false,
"properties": [
{ "$id": "9", "name": "ActivityOutput", "expressions": { "$id": "10" } },
{ "$id": "11", "name": "OutcomeNames", "expressions": { "$id": "12" } }
],
"propertyStorageProviders": { "$id": "13" }
},
{
"$id": "14",
"activityId": "b6fdf404-6b75-48fd-9b60-b6b9719de88b",
"type": "Finish",
"displayName": "Finish",
"persistWorkflow": false,
"loadWorkflowContext": false,
"saveWorkflowContext": false,
"properties": [
{ "$id": "15", "name": "ActivityOutput", "expressions": { "$id": "16" } },
{ "$id": "17", "name": "OutcomeNames", "expressions": { "$id": "18" } }
],
"propertyStorageProviders": { "$id": "19" }
}
],
"connections": [
{
"$id": "20",
"sourceActivityId": "5762415e-46fa-47b3-9f64-c5cdfb737091",
"targetActivityId": "b6fdf404-6b75-48fd-9b60-b6b9719de88b",
"outcome": "Done"
}
],
"id": "178b6e9eaf2b4023b4b54101099bc514"
}
```

4. **Reproduction Rate**: every time (singleton `ElsaContextFactory`).

## Expected Behavior
Either of:
- The connection string is resolved from IConfiguration at DbContext creation / connection open time, so configuration reload is honored without a restart; or
- An extension point for passing the current connection string at use time (see below)
- The serviceLifetime parameter of UseNonPooledEntityFrameworkPersistence affect the observed behavior

## Actual Behavior
The configure delegate executes exactly once.
All stores use the connection string captured at that moment forever.
Configuration changes from a provider that raises OnReload() are never observed.
In rotation environments this manifests as: per-minute `Login failed for user` on the WorkflowUnfinishedStatusSpecification query (WorkflowLaunchpad when the Quartz cron fires ->SQL account lockout -> death of the recurring trigger until app restart - not sure about the last one)

## Environment
- **Elsa Package Version**: 2.x
- **Operating System**: Windows
- **Browser and Version**: N/A

## Log Output
Example Output of the many not sure if this is cron or startWorkflow but behaviour is the same
```json
fail: Microsoft.EntityFrameworkCore.Database.Connection[20004]
An error occurred using the connection to database 'NONEXISTING' on server '(localdb)\MSSQLLocalDB'.
fail: Microsoft.EntityFrameworkCore.Query[10100]
An exception occurred while iterating over the results of a query for context type 'Elsa.Persistence.EntityFramework.Core.ElsaContext'.
System.InvalidOperationException: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot open database "NONEXISTING" requested by the login. The login failed.
Login failed for user 'AzureAD\NikosTriantafyllou'.
at Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
at Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 taskCompletionSource, DbConnectionOptions userOptions, DbConnectionInternal& connection)
at Microsoft.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
at Microsoft.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
at Microsoft.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
at Microsoft.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry, SqlConnectionOverrides overrides)
at Microsoft.Data.SqlClient.SqlConnection.InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
ClientConnectionId:0aa0fcf9-e4f6-46fd-aa8a-f98d2681d5b9
Error Number:4060,State:1,Class:11
--- End of inner exception stack trace ---
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
System.InvalidOperationException: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot open database "NONEXISTING" requested by the login. The login failed.
Login failed for user 'AzureAD\NikosTriantafyllou'.
at Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
at Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 taskCompletionSource, DbConnectionOptions userOptions, DbConnectionInternal& connection)
at Microsoft.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
at Microsoft.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
at Microsoft.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
at Microsoft.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry, SqlConnectionOverrides overrides)
at Microsoft.Data.SqlClient.SqlConnection.InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
ClientConnectionId:0aa0fcf9-e4f6-46fd-aa8a-f98d2681d5b9
Error Number:4060,State:1,Class:11
--- End of inner exception stack trace ---
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
warn: Elsa.Services.Workflows.WorkflowRunner[0]
Failed to run workflow 088d3fe7563740df97d2cba85f5a9a2c
System.InvalidOperationException: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot open database "NONEXISTING" requested by the login. The login failed.
Login failed for user 'AzureAD\NikosTriantafyllou'.
at Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
at Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 taskCompletionSource, DbConnectionOptions userOptions, DbConnectionInternal& connection)
at Microsoft.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
at Microsoft.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
at Microsoft.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
at Microsoft.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry, SqlConnectionOverrides overrides)
at Microsoft.Data.SqlClient.SqlConnection.InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternalAsync(Boolean errorsExpected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenAsync(CancellationToken cancellationToken, Boolean errorsExpected)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
ClientConnectionId:0aa0fcf9-e4f6-46fd-aa8a-f98d2681d5b9
Error Number:4060,State:1,Class:11
--- End of inner exception stack trace ---
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Elsa.Persistence.EntityFramework.Core.Stores.EntityFrameworkStore`2.<>c__DisplayClass7_0.<b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Elsa.Persistence.EntityFramework.Core.Stores.EntityFrameworkStore`2.DoWork(Func`2 work, CancellationToken cancellationToken)
at Elsa.Persistence.EntityFramework.Core.Stores.EntityFrameworkStore`2.DoWork(Func`2 work, CancellationToken cancellationToken)
at Elsa.Persistence.EntityFramework.Core.Stores.EntityFrameworkStore`2.SaveAsync(T entity, CancellationToken cancellationToken)
at Elsa.Persistence.Decorators.EventPublishingWorkflowInstanceStore.SaveAsync(WorkflowInstance entity, CancellationToken cancellationToken)
at Elsa.Handlers.PersistWorkflow.SaveWorkflowAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken)
at Elsa.Handlers.PersistWorkflow.Handle(WorkflowStatusChanged notification, CancellationToken cancellationToken)
at MediatR.NotificationPublishers.ForeachAwaitPublisher.Publish(IEnumerable`1 handlerExecutors, INotification notification, CancellationToken cancellationToken)
at Elsa.Services.Workflows.WorkflowRunner.BeginWorkflow(WorkflowExecutionContext workflowExecutionContext, IActivityBlueprint activity, CancellationToken cancellationToken)

```

## Troubleshooting Attempts
1. Reading the connection string inside the configure delegate - no effect; the delegate itself only runs once, because DbContextOptions is registered as a singleton service descriptor.
2. UseNonPooledEntityFrameworkPersistence(configure, ServiceLifetime.Transient or Scoped) - no effect. The singleton ElsaContextFactory resolves IDbContextFactory once in its constructor,

## Additional Context
Two working solutions I came up with.

# Solution 1 — make ElsaContextFactory hold the configure delegate and rebuild options per CreateDbContext().
```csharp
internal sealed class ReloadedElsaContextFactory : IElsaContextFactory {
private readonly IServiceProvider _serviceProvider;
private readonly Action _configure;

public LiveConfigElsaContextFactory(
IServiceProvider serviceProvider,
Action configure) {
_serviceProvider = serviceProvider;
_configure = configure;
}

public ElsaContext CreateDbContext() {
var builder = new DbContextOptionsBuilder();
_configure(_serviceProvider, builder);
return new ElsaContext(builder.Options);
}
}
```

```csharp
builder.Services.AddSingleton(sp =>
new ReloadedElsaContextFactory (sp, (provider, ef) =>
ef.UseSqlServer(provider.GetRequiredService().GetConnectionString("WorkflowDb"))));
```
Essentially passing `configure` into ElsaContextFactory would mitigate the issue - manually tested.

Trade-off: This is bypassing the whole pooling path altogether as it recreates dbContexts so it should be available only in nonpooling.

# Solution 2 connection-open interceptor
Re-resolve the connection at physical connection open
```csharp
var configureDatabase = new Action((sp, ef) => ef
.UseSqlServer(sp.GetRequiredService().GetConnectionString("WorkflowDb"))
.AddInterceptors(new RotatingConnectionStringInterceptor(sp.GetRequiredService(), "WorkflowDb")));
```
```csharp
internal class RotatingConnectionStringInterceptor(IConfiguration configuration, string connectionStringName) : DbConnectionInterceptor
{
public override InterceptionResult ConnectionOpening(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result
) {
RefreshConnectionString(connection);
return base.ConnectionOpening(connection, eventData, result);
}

public override ValueTask ConnectionOpeningAsync(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default
) {
RefreshConnectionString(connection);
return base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken);
}

private void RefreshConnectionString(DbConnection connection) {
if (connection.State != ConnectionState.Closed) {
return;
}

var current = configuration.GetConnectionString(connectionStringName);
if (!string.IsNullOrWhiteSpace(current)) {
connection.ConnectionString = current;
}
}
}
```
Works with both pooled and non-pooled registrations.

# Solution 3
Create a CachingElsaContextFactory with IOptionsMonitor or simply ChangeToken.OnChange to recreate the dbContext on change only .

# Conclusion
Don't know if the issue affects Elsa 3.x as well.
The rotating connection string issue is a valid enterprise requirement. I understand this can be solved with various methods in Azure or on prem SSPI connection strings but still having a code first solution is better.

# Proposed Contribution
If the maintainers are open to it, I'd be glad to submit a PR against the 2.x branch containing:
- Regression test reproducing the freeze.
- A new opt-in overload of UseNonPooledEntityFrameworkPersistence that passes the configure delegate into the IElsaContextFactory registration, so options are rebuilt per context creation.
- Worth noting: the existing source comment in UseEntityFrameworkPersistence already states "(IE: Contexts might not
* all connect to the same DB)."

Or go with option #3
Or go with the Interceptor path not sure what is preferred.

Contributor guide

Open the contributing guide

Research direction

Start by tracing UseEntityFrameworkPersistence, ElsaContextFactory, and the IDbContextFactory registrations described in the issue, comparing pooled and non-pooled behavior and service lifetimes. Reproduce the delayed IConfiguration reload, then verify that Elsa stores observe the current connection string when creating or opening a DbContext without requiring an application restart.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, sql
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.