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
Storage Instances and Data Categories
Section titled “Storage Instances and Data Categories”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:
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:
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
IdentityServer 8.0 Storage Setup
Section titled “IdentityServer 8.0 Storage Setup”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:
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.
Choose a Database
Section titled “Choose a Database”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:
# Terminaldotnet add package Duende.Storage.Sqlite# For PostgreSQL:dotnet add package Duende.Storage.Postgresql# For SQL Server:dotnet add package Duende.Storage.MssqlFor 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.
Migrate the User Management Database
Section titled “Migrate the User Management Database”Call MigrateAsync to create or upgrade the schema for the storage instance User Management uses. To migrate the
default storage instance:
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:
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.