Skip to content
New Livestream: OWASP Top 10 2025 Explained for .NET Developers. Register Now!

Getting Started with Duende Storage

AddStorage(...) selects a database provider for a storage instance. AddConfigurationStorage() and AddOperationalStorage() then route IdentityServer’s configuration and operational data to an instance and register the matching stores. By default, all three target the same, default instance, so a typical application uses one database. See Multiple Storage Instances to route categories to separate databases.

Install the IdentityServer preview and one database provider. This walkthrough uses SQLite, which is suitable for automated tests and local development only. For production, use SQL Server, PostgreSQL or Oracle; see the Databases overview for the provider packages and registration methods.

Terminal window
# Terminal
dotnet add package Duende.IdentityServer --prerelease
dotnet add package Duende.Storage.Sqlite --prerelease

The packages are available from the Duende.IdentityServer and Duende.Storage.Sqlite NuGet Gallery pages.

Add a connection string for the provider:

appsettings.json
{
"ConnectionStrings": {
"IdentityServer": "Data Source=identityserver.db"
}
}

Keep production credentials out of source control and load them from your deployment platform’s secret store.

Call AddStorage(...) on the IIdentityServerBuilder to select a database provider for the default storage instance, then call AddConfigurationStorage() and AddOperationalStorage() to route configuration and operational data to it:

Program.cs
using Duende.IdentityServer;
using Duende.Storage.Schema;
using Duende.Storage.Sqlite;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddIdentityServer()
.AddStorage(storage =>
storage.AddSqlite(options =>
options.ConnectionString =
builder.Configuration.GetConnectionString("IdentityServer")
?? throw new InvalidOperationException(
"IdentityServer connection string is missing.")))
.AddConfigurationStorage()
.AddOperationalStorage();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
await app.Services
.GetRequiredService<IStorageInstanceSchema>()
.MigrateAsync(CancellationToken.None);
}
app.UseIdentityServer();
app.Run();

Do not hide a missing connection string or continue startup after a migration failure.

The example runs migrations from the application only in development. See Deploy the Database Schema for the production approach.

AddConfigurationStorage() registers storage-backed implementations of:

  • IClientStore
  • IResourceStore
  • IIdentityProviderStore
  • ISamlServiceProviderStore
  • ICorsPolicyService

It also registers the configuration administration APIs.

AddOperationalStorage() also registers:

  • IPersistedGrantStore
  • IDeviceFlowStore
  • IPushedAuthorizationRequestStore
  • IServerSideSessionStore
  • ISigningKeyStore
  • ISamlSigninStateStore
  • ISamlLogoutSessionStore

Call AddServerSideSessions separately when you want IdentityServer to use server-side sessions.

The provider adds a background purge service that runs against every registered storage instance. Purging is enabled by default, runs hourly, deletes 100 expired entities per batch and fuzzes its initial start time to reduce collisions between nodes. Configure StoragePurgeOptions before calling AddOperationalStorage() to tune those values:

Program.cs
using Duende.IdentityServer.Configuration;
builder.Services.Configure<StoragePurgeOptions>(options =>
{
options.PurgeInterval = TimeSpan.FromMinutes(30); // Default: 60 minutes
options.BatchSize = 200; // Default: 100
});

Set EnablePurge to false when an external job owns cleanup.

Call an explicit store registration after AddConfigurationStorage()/AddOperationalStorage() to replace only that store. For example:

Program.cs
builder.Services
.AddIdentityServer()
.AddStorage(storage =>
storage.AddSqlite(options =>
options.ConnectionString = connectionString))
.AddConfigurationStorage()
.AddOperationalStorage()
.AddInMemoryClients(clients);

This replaces the Duende Storage-backed client store while leaving the operational and other configuration stores in Duende Storage.

For local development, you can use the SQLite CLI command:

Terminal window
# Terminal
dotnet tool install --global Duende.Cli --prerelease
duende storage migrate --provider sqlite --connection-string "Data Source=identityserver.db"

For production, use SQL Server or PostgreSQL and run the migration as a single deployment step before application instances start. Configure DUENDE_STORAGE_CONNECTION_STRING in your deployment environment’s secret store, then run the CLI from a restored application project that references the matching storage provider package:

Terminal window
# Terminal
dotnet tool install --global Duende.Cli --prerelease
duende storage migrate --provider postgresql

The CLI reads the connection string from DUENDE_STORAGE_CONNECTION_STRING. Do not pass production credentials as command-line arguments. For plugin version resolution and the full command reference, see the Duende CLI documentation. See the Duende CLI reference for the other supported providers and options. The CLI doesn’t support Oracle migrations yet; see the Oracle page for the code-based path.

Run only one migration process at a time. After applying a migration, MigrateAsync verifies that the database matches the expected schema and fails when it finds discrepancies.

Operational records contain tokens, grants, session data and signing material. Restrict database access, encrypt connections and backups and avoid logging stored payloads or secrets.

Duende User Management uses the same Duende Storage abstractions as IdentityServer. When AddStorage(...) has already registered a provider for the default instance, call AddUserManagement(...) without registering the same provider a second time: it maps the user-management data category to that instance and reuses its database.

Program.cs
builder.Services
.AddIdentityServer()
.AddStorage(storage =>
storage.AddSqlite(options =>
options.ConnectionString = connectionString))
.AddConfigurationStorage()
.AddOperationalStorage()
.AddUserManagement(_ => { });

IdentityServer and User Management can share the physical database, connection string and common Duende Storage schema. Their entities use different entity types within the storage layer.

When Spaces is enabled, User Management repositories use the same space-aware storage factory as IdentityServer. User profiles, authenticators, roles and groups are therefore stored in the resolved space’s pool by default.

If users must be shared globally across spaces, do not rely on this default routing. Use a deliberately separate host or storage architecture for the shared user directory. The built-in integration does not provide a per-product switch that opts only User Management out of the current space pool.

To store IdentityServer and User Management data in separate databases instead of sharing one, see Multiple Storage Instances.

For a complete, runnable IdentityServer application that stores and administers configuration and operational data with Duende Storage, see the Storage sample.