Getting Started With Spaces
Spaces require Duende Storage for their management data and isolated storage pools.
Install Duende.Spaces Packages
Section titled “Install Duende.Spaces Packages”This example uses SQLite:
# Terminaldotnet add package Duende.IdentityServer --prereleasedotnet add package Duende.Storage.Sqlite --prereleasedotnet add package Duende.Spaces --prereleaseSee the Duende.Spaces NuGet Gallery page for package versions.
Add a Connection String
Section titled “Add a Connection String”Add a connection string for the SQLite database:
{ "ConnectionStrings": { "IdentityServer": "Data Source=identityserver.db" }}Keep production credentials out of source control and load them from your deployment platform’s secret store.
Configure IdentityServer with Spaces
Section titled “Configure IdentityServer with Spaces”Register Spaces and the IdentityServer storage adapters. AddStorage selects the database provider;
AddConfigurationStorage() and AddOperationalStorage() route configuration and operational data to it. Add Spaces
with the AddSpaces extension on IIdentityServerBuilder:
using System.Net;using Duende.IdentityServer;using Duende.Spaces;using Duende.Storage.Schema;using Duende.Storage.Sqlite;using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
builder.Services .AddIdentityServer() .AddServerSideSessions() .AddStorage(storage => storage.AddSqlite(options => options.ConnectionString = builder.Configuration.GetConnectionString("IdentityServer") ?? throw new InvalidOperationException( "IdentityServer connection string is missing."))) .AddConfigurationStorage() .AddOperationalStorage() .AddSpaces(options => { options.SpacePathPrefix = "/t"; options.FallbackToDefault = false; });
builder.Services.Configure<ForwardedHeadersOptions>(options =>{ options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; options.KnownProxies.Add(IPAddress.Parse("203.0.113.42")); options.ForwardLimit = 1;});
var app = builder.Build();
await app.Services .GetRequiredService<IStorageInstanceSchema>() .MigrateAsync(CancellationToken.None);FallbackToDefault is already false; setting it explicitly makes the intended isolation behavior visible during review.
Create A Space
Section titled “Create A Space”Use ISpaceAdmin from a trusted provisioning or administration path. Query by name before creating the space:
using Duende.Storage.Querying;
var spaces = app.Services.GetRequiredService<ISpaceAdmin>();
var existing = await spaces.QueryAsync( QueryRequest.Create<SpaceFilter, SpaceSortField>( new SpaceFilter { Name = "Acme" }), CancellationToken.None);
if (!existing.Items.Any(space => string.Equals(space.Name, "Acme", StringComparison.Ordinal))){ var result = await spaces.CreateAsync( new CreateSpaceConfiguration { Name = "Acme", MatchPatterns = [ new SpaceMatchPattern { Origin = "https://login.example.com", Path = "/acme" } ] }, CancellationToken.None);
if (!result.IsSuccess) { throw new InvalidOperationException( $"Could not create the Acme space: {result.Errors}"); }}The Acme space created in this example requires both the origin and path to match. A request to
https://login.example.com/t/acme/.well-known/openid-configuration resolves to the Acme pool.
Add Space Resolution Middleware
Section titled “Add Space Resolution Middleware”Forwarded headers must run before space resolution so the resolver sees the public scheme and host. Space resolution must
then run before ASP.NET Core routing because path-based matches rewrite PathBase and Path:
app.UseForwardedHeaders();app.UseSpaceResolution();app.UseRouting();
app.UseIdentityServer();
app.Run();If another middleware reads tenant-specific data, place it after UseSpaceResolution. You can inject
ISpaceContextAccessor into scoped services and call GetSpaceId() after resolution.
Replace 203.0.113.42 with your proxy’s address or configure an appropriate trusted network. Only accept forwarded
headers from expected proxies or networks. An untrusted forwarded host or protocol can otherwise influence which security
boundary the request selects. See Proxy Servers and Load Balancers
for more configuration options.
Choose Match Patterns
Section titled “Choose Match Patterns”- Use origin matching when each space has a dedicated host name.
- Use path matching when spaces share a host. The default
/tprefix keeps space paths separate from ordinary routes. - Use both when a space must be constrained to a specific host and path.
Origins must include the scheme and host, plus the port when it is not the scheme default.
Before Deployment
Section titled “Before Deployment”Review what Spaces does not isolate before deploying. Confirm whether signing credentials, Data Protection, caches, telemetry, rate limits and custom services require space-aware configuration.
Spaces partitions ASP.NET Core Data Protection by space by default
(SpacesOptions.PartitionDataProtection, default true): a protector created from the app’s IDataProtectionProvider
binds to the space resolved when you call Protect or Unprotect, so a value protected in one space can’t be
unprotected in another. IdentityServer’s own signing key protection stays global across spaces because it calls
GetUnpartitioned() on the provider. Use GetUnpartitioned() yourself for data that must be readable from every
space; see Data Protection for background on providers and protectors.
For the Default space fallback, failures outside a resolved space, and which providers are partitioned, see Data Protection In Spaces.
Set SpacesOptions.PartitionDataProtection to false when all protected data from the app’s
IDataProtectionProvider, such as cookies or tokens, must be readable across spaces. This removes per-space
partitioning for that data. If only specific values need to be shared, use GetUnpartitioned() for those protectors
instead.
Spaces registers its own keyed HybridCache under SpacesServiceProviderKeys.SpacesCache instead of registering or
sharing the app’s default HybridCache. If your app already relies on an unkeyed HybridCache for its own caching,
call AddHybridCache() yourself. Entries created through IHybridCacheFactory are automatically partitioned by the
resolved space, so callers don’t need to build space-aware cache keys themselves.
Space resolution results are cached in the Spaces HybridCache. SpacesOptions.LocalCacheExpiration sets the
in-process cache lifetime (default 30 seconds), and SpacesOptions.Expiration sets the distributed cache lifetime
(default 30 minutes). A space change is visible immediately on the instance that made it. Other instances see it once
their in-process cache entry expires, after at most LocalCacheExpiration.
Spaces management data and every space’s pool share the default Duende Storage instance unless you map them elsewhere. See Multiple Storage Instances to route IdentityServer’s configuration or operational data, or User Management’s data, to a separate database from Spaces management data.
Sample
Section titled “Sample”For a complete, runnable multi-space deployment, see the Spaces sample.