Skip to content

Storage Configuration

Duende User Management stores users, authenticators, roles, and groups as documents. Because entities are stored as complete documents, adding or removing properties on a user profile does not require a database schema change.

IdentityServer 8.1 and later

The IdentityServer 8.1 integration stores these documents in Duende Storage. The storage tables themselves are created and upgraded by Duende Storage migrations. See Create and Upgrade the Schema. All Duende Storage databases are supported. See Databases for the full list.

This page covers User Management data — user profiles, authenticators, roles, and groups. It does not govern how IdentityServer stores its configuration data (clients, API scopes, API resources, and identity resources) or operational data (persisted grants, device flow codes, signing keys). IdentityServer configuration continues to use whichever provider you have configured, such as Entity Framework Core with its classic relational tables.

IdentityServer 8.1 and later

It can also use the Duende Storage provider.

IdentityServer 8.1 and later

Duende Storage registers databases as named storage instances. Each call to AddStorage(...) on the IdentityServer builder makes one database available under a StorageInstanceId. Separately, each product maps its own data to an instance: User Management maps its data category when you call AddUserManagement(...), and IdentityServer maps configuration and operational data when you call AddConfigurationStorage() and AddOperationalStorage(). A category that is never mapped to a named instance falls back to the default instance (StorageInstanceId.Default).

This split means AddStorage(...) only selects a database provider and connection; it does not, by itself, store anything. You still need to tell each product which instance to use. Selecting a second provider for the same instance throws an InvalidOperationException.

By default, AddUserManagement(...) stores its data in StorageInstanceId.Default, alongside whatever IdentityServer data you have routed there:

Program.cs
using Duende.IdentityServer;
using Duende.Storage.PostgreSql;
using Npgsql;
builder.Services
.AddSingleton(new NpgsqlDataSourceBuilder(
builder.Configuration.GetConnectionString("pgsql")!).Build());
builder.Services
.AddIdentityServer()
.AddStorage(storage => storage.AddPostgreSql())
.AddConfigurationStorage()
.AddOperationalStorage()
.AddUserManagement(um => { /* ... */ });

Here, clients, API scopes, persisted grants, and User Management’s users, profiles, and authenticators all live in the same PostgreSQL database.

Using A Dedicated Database For User Management

Section titled “Using A Dedicated Database For User Management”

For larger deployments, or when User Management’s write volume (OTP challenges, passkey ceremonies, authentication attempts) would compete with IdentityServer’s configuration and operational workloads, register a second storage instance and map User Management to it with StorageInstanceId:

Program.cs
using Duende.IdentityServer;
using Duende.Storage;
using Duende.Storage.PostgreSql;
using Npgsql;
var userManagementInstance = StorageInstanceId.Create("user-management");
builder.Services
.AddSingleton(new NpgsqlDataSourceBuilder(
builder.Configuration.GetConnectionString("pgsql")!).Build())
.AddKeyedSingleton(userManagementInstance.Value, (sp, _) =>
new NpgsqlDataSourceBuilder(
builder.Configuration.GetConnectionString("pgsql-usermanagement")!).Build());
builder.Services
.AddIdentityServer()
// IdentityServer's configuration and operational data use the default instance.
.AddStorage(storage => storage.AddPostgreSql())
.AddConfigurationStorage()
.AddOperationalStorage()
// User Management uses a dedicated database, registered as its own instance.
.AddStorage(userManagementInstance, storage => storage.AddPostgreSql(
sp => sp.GetRequiredKeyedService<NpgsqlDataSource>(userManagementInstance.Value)))
.AddUserManagement(userManagementInstance, um => { /* ... */ });

AddStorage(storageInstanceId, configure) only makes a database available under that instance; it does not route any data to it. AddUserManagement(storageInstanceId, configure) is what tells User Management to use it. Because each instance is a separate database, it is migrated separately — see Migrate the User Management Database below for migrating multiple instances. For the general model, including how IdentityServer, User Management and Spaces data can each be placed in different instances, see the Multiple Storage Instances.

IdentityServer before 8.1

In IdentityServer 8.0, register the storage provider for User Management and then call AddUserManagement(...). IdentityServer configuration and operational data continue to use their separately configured stores.

For example, register a PostgreSQL data source and use it for User Management:

Program.cs
using Duende.IdentityServer;
using Duende.Storage.PostgreSql;
using Npgsql;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(new NpgsqlDataSourceBuilder(
builder.Configuration.GetConnectionString("pgsql")!).Build());
builder.Services
.AddIdentityServer()
.AddStorage(storage => storage.AddPostgreSql())
.AddUserManagement(um => { /* ... */ });

For other provider package names and registration options, see the database choices below.

IdentityServer before 8.1

For IdentityServer 8.0, use the in-memory provider for local development and tests, or PostgreSQL and SQL Server for production. Oracle support for User Management data starts with IdentityServer 8.1.

Install the provider package you use:

Terminal window
# Terminal
dotnet add package Duende.Storage.Sqlite
# For PostgreSQL:
dotnet add package Duende.Storage.Postgresql
# For SQL Server:
dotnet add package Duende.Storage.Mssql

For PostgreSQL, register an NpgsqlDataSource and select the provider with AddStorage(storage => storage.AddPostgreSql()). For SQL Server, register a CreateSqlConnection factory and select it with AddStorage(storage => storage.AddMsSql(options => { })). For local development and testing, use AddStorage(storage => storage.AddSqliteInMemory()) or configure a file-based SQLite connection string.

IdentityServer 8.1 and later

SQL Server, PostgreSQL, and Oracle are production databases for User Management data; pick whichever fits your infrastructure. SQLite is for local development and automated tests — use AddSqliteInMemory() to run tests against an in-memory SQLite database with no external dependencies.

Call MigrateAsync to create or upgrade the schema for the storage instance User Management uses. To migrate the default storage instance:

Program.cs
using Duende.Storage.Schema;
using var scope = app.Services.CreateScope();
var schemaFactory = scope.ServiceProvider.GetRequiredService<IStorageInstanceSchemaFactory>();
var schema = await schemaFactory.GetStorageInstanceSchema(CancellationToken.None);
await schema.MigrateAsync(CancellationToken.None);

IdentityServer 8.1 and later

When User Management shares the default instance with IdentityServer, migrate that same instance. When User Management uses a dedicated storage instance (see Using A Dedicated Database For User Management above), also migrate that instance by passing its StorageInstanceId:

Program.cs
var schema = await schemaFactory.GetStorageInstanceSchema(userManagementInstance, CancellationToken.None);
await schema.MigrateAsync(CancellationToken.None);

Calling MigrateAsync() at application startup, as shown above, is convenient for development, but production deployments should run migrations as a separate step.

IdentityServer 8.1 and later

For SQL Server, PostgreSQL, and Oracle, run migrations as a deployment step with the Duende CLI, once per connection string.

IdentityServer before 8.1

In production, use deployment tooling to run MigrateAsync as a single step before application instances start, once per connection string.

Run only one migration process at a time against a given database.

IdentityServer 8.1 and later

Running startup migration and the CLI concurrently against the same connection string is not supported.