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 Duende Storage NuGet Packages
Section titled “Install Duende Storage NuGet Packages”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.
# Terminaldotnet add package Duende.IdentityServer --prereleasedotnet add package Duende.Storage.Sqlite --prereleaseThe packages are available from the Duende.IdentityServer and Duende.Storage.Sqlite NuGet Gallery pages.
Add a Connection String
Section titled “Add a Connection String”Add a connection string for the provider:
{ "ConnectionStrings": { "IdentityServer": "Data Source=identityserver.db" }}Keep production credentials out of source control and load them from your deployment platform’s secret store.
Register Duende Storage
Section titled “Register Duende Storage”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:
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.
Configuration Stores
Section titled “Configuration Stores”AddConfigurationStorage() registers storage-backed implementations of:
IClientStoreIResourceStoreIIdentityProviderStoreISamlServiceProviderStoreICorsPolicyService
It also registers the configuration administration APIs.
Operational Stores
Section titled “Operational Stores”AddOperationalStorage() also registers:
IPersistedGrantStoreIDeviceFlowStoreIPushedAuthorizationRequestStoreIServerSideSessionStoreISigningKeyStoreISamlSigninStateStoreISamlLogoutSessionStore
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:
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.
Override an Individual Store
Section titled “Override an Individual Store”Call an explicit store registration after AddConfigurationStorage()/AddOperationalStorage() to replace only that
store. For example:
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.
Deploy the Database Schema
Section titled “Deploy the Database Schema”For local development, you can use the SQLite CLI command:
# Terminaldotnet 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:
# Terminaldotnet tool install --global Duende.Cli --prerelease
duende storage migrate --provider postgresqlThe 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.
Protect Stored Data
Section titled “Protect Stored Data”Operational records contain tokens, grants, session data and signing material. Restrict database access, encrypt connections and backups and avoid logging stored payloads or secrets.
Use Duende User Management
Section titled “Use Duende User Management”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.
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.
Sample
Section titled “Sample”For a complete, runnable IdentityServer application that stores and administers configuration and operational data with Duende Storage, see the Storage sample.